Skip to content

Commit 03a8b93

Browse files
Merge pull request #36 from yash-pouranik/feat/auth-system-dx-upgrade
feat: implement major auth system upgrade with dynamic forms and enha…
2 parents 8da98af + 8ed4f89 commit 03a8b93

21 files changed

Lines changed: 1427 additions & 240 deletions

backend/app.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { getPublicIp } = require('./utils/network');
1818

1919
// Initialize Queue Workers
2020
require('./queues/emailQueue');
21+
require('./queues/authEmailQueue');
2122

2223
// Middleware (We apply admin options globally except where overridden via dynamic handling)
2324
app.use(express.json());
@@ -78,9 +79,7 @@ const schemaRoute = require('./routes/schemas');
7879
const releaseRoute = require('./routes/releases');
7980

8081
// ROUTES SETUP
81-
app.use('/api/auth/login', cors(adminCorsOptions), authLimiter); // Strict limiter on login
82-
app.use('/api/auth/register', cors(adminCorsOptions), authLimiter); // Strict limiter on register
83-
app.use('/api/auth', cors(adminCorsOptions), dashboardLimiter, authRoute); // Developer Auth (general)
82+
app.use('/api/auth', cors(adminCorsOptions), authRoute); // Developer Auth (general)
8483
app.use('/api/projects', cors(adminCorsOptions), dashboardLimiter, projectRoute); // Project Mgmt
8584
app.use('/api/userAuth', cors(adminCorsOptions), limiter, logger, userAuthRoute);
8685
app.use('/api/releases', cors(adminCorsOptions), releaseRoute);

backend/controllers/project.controller.js

Lines changed: 159 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,16 @@ const { getCompiledModel } = require("../utils/injectModel")
1414
const QueryEngine = require("../utils/queryEngine");
1515
const { storageRegistry } = require("../utils/registry");
1616
const { deleteProjectByApiKeyCache, setProjectById, getProjectById, deleteProjectById } = require("../services/redisCaching");
17+
const { v4: uuidv4 } = require('uuid');
1718
const { getPublicIp } = require("../utils/network");
1819

20+
const validateUsersSchema = (schema) => {
21+
if (!Array.isArray(schema)) return false;
22+
const hasEmail = schema.find(f => f.key === 'email' && f.type === 'String' && f.required);
23+
const hasPassword = schema.find(f => f.key === 'password' && f.type === 'String' && f.required);
24+
return !!(hasEmail && hasPassword);
25+
};
26+
1927

2028

2129
const getBucket = (project) =>
@@ -95,20 +103,38 @@ module.exports.getAllProject = async (req, res) => {
95103

96104
module.exports.getSingleProject = async (req, res) => {
97105
try {
98-
let project;
99-
project = await getProjectById(req.params.projectId);
100-
let projectObj;
101-
if (!project) {
102-
project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-publishableKey -secretKey -jwtSecret');
106+
let projectObj = await getProjectById(req.params.projectId);
107+
108+
if (!projectObj) {
109+
const project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-publishableKey -secretKey -jwtSecret');
103110
if (!project) return res.status(404).json({ error: "Project not found." });
104111
projectObj = project.toObject();
105-
await setProjectById(req.params.projectId, project);
112+
await setProjectById(req.params.projectId, projectObj);
113+
}
114+
115+
// Ownership Check (Even for Cache)
116+
if (projectObj.owner.toString() !== req.user._id.toString()) {
117+
return res.status(403).json({ error: "Access denied." });
106118
}
107119

108-
projectObj = project;
120+
// Just to be safe, we remove sensitive fields again even if from cache
109121
delete projectObj.publishableKey;
110122
delete projectObj.secretKey;
111123
delete projectObj.jwtSecret;
124+
125+
// SANITIZE COLLECTION MODELS: Remove "password" from "users" collection schema
126+
if (projectObj.collections && Array.isArray(projectObj.collections)) {
127+
projectObj.collections = projectObj.collections.map(col => {
128+
if (col.name === 'users' && col.model) {
129+
return {
130+
...col,
131+
model: col.model.filter(m => m.key !== 'password')
132+
};
133+
}
134+
return col;
135+
});
136+
}
137+
112138
res.json(projectObj);
113139
} catch (err) {
114140
res.status(500).json({ error: err.message });
@@ -283,6 +309,7 @@ module.exports.deleteExternalStorageConfig = async (req, res) => {
283309

284310

285311

312+
// POST REQ FOR CREATE COLLECTION
286313
module.exports.createCollection = async (req, res) => {
287314
try {
288315
const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body);
@@ -295,14 +322,19 @@ module.exports.createCollection = async (req, res) => {
295322

296323
if (!project.jwtSecret) project.jwtSecret = uuidv4();
297324

325+
if (collectionName === 'users') {
326+
if (!validateUsersSchema(schema)) {
327+
return res.status(422).json({ error: "The 'users' collection must have required 'email' and 'password' string fields." });
328+
}
329+
}
330+
298331
project.collections.push({ name: collectionName, model: schema });
299332
await project.save();
300333

301334
await deleteProjectById(projectId);
302335
await setProjectById(projectId, project);
303336
await deleteProjectByApiKeyCache(project.publishableKey);
304337
await deleteProjectByApiKeyCache(project.secretKey);
305-
// RESPONSE
306338
const projectObj = project.toObject();
307339
delete projectObj.publishableKey;
308340
delete projectObj.secretKey;
@@ -371,7 +403,12 @@ module.exports.getData = async (req, res) => {
371403

372404
// const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray();
373405

374-
const features = new QueryEngine(model.find(), req.query)
406+
const query = model.find();
407+
if (collectionName === 'users') {
408+
query.select('-password');
409+
}
410+
411+
const features = new QueryEngine(query, req.query)
375412
.filter()
376413
.sort()
377414
.paginate();
@@ -391,6 +428,10 @@ module.exports.insertData = async (req, res) => {
391428
const project = await Project.findOne({ _id: projectId, owner: req.user._id });
392429
if (!project) return res.status(404).json({ error: "Project not found." });
393430

431+
if (collectionName === 'users') {
432+
return res.status(400).json({ error: "Direct inserts into 'users' collection are not allowed. Please use the Auth signup or admin endpoints." });
433+
}
434+
394435
const finalCollectionName = `${project._id}_${collectionName}`;
395436
const incomingData = req.body;
396437

@@ -419,7 +460,6 @@ module.exports.insertData = async (req, res) => {
419460
project.databaseUsed = (project.databaseUsed || 0) + docSize;
420461
}
421462
await project.save();
422-
await project.save();
423463

424464
res.json(result);
425465
} catch (err) {
@@ -480,6 +520,14 @@ module.exports.editRow = async (req, res) => {
480520
const connection = await getConnection(projectId);
481521
const Model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal);
482522

523+
if (collectionName === 'users') {
524+
delete req.body.password;
525+
// Also ensure it's not and nested or sneaky
526+
Object.keys(req.body).forEach(key => {
527+
if (key.toLowerCase().includes('password')) delete req.body[key];
528+
});
529+
}
530+
483531
const docToEdit = await Model.findById(id);
484532
if (!docToEdit) {
485533
return res.status(404).json({ error: "Document not found." });
@@ -505,8 +553,12 @@ module.exports.editRow = async (req, res) => {
505553
}
506554

507555
const updatedDoc = await docToEdit.save();
556+
const responseData = updatedDoc.toObject();
557+
if (collectionName === 'users') {
558+
delete responseData.password;
559+
}
508560

509-
res.json({ success: true, message: "Document edited successfully", data: updatedDoc });
561+
res.json({ success: true, message: "Document edited successfully", data: responseData });
510562

511563
} catch (err) {
512564
console.error("Edit Error:", err);
@@ -754,35 +806,41 @@ module.exports.deleteProject = async (req, res) => {
754806
if (!project) {
755807
return res.status(404).json({ error: "Project not found or access denied." });
756808
}
757-
for (const col of project.collections) {
758-
const collectionName = `${project._id}_${col.name}`;
809+
810+
// DROP COLLECTIONS: Only for internal databases
811+
if (!project.resources.db.isExternal) {
812+
for (const col of project.collections) {
813+
const collectionName = `${project._id}_${col.name}`;
814+
try {
815+
await mongoose.connection.db.dropCollection(collectionName);
816+
} catch (e) { }
817+
}
818+
759819
try {
760-
await mongoose.connection.db.dropCollection(collectionName);
820+
await mongoose.connection.db.dropCollection(`${project._id}_users`);
761821
} catch (e) { }
762822
}
763823

764-
try {
765-
await mongoose.connection.db.dropCollection(`${project._id}_users`);
766-
} catch (e) { }
767-
768-
// DELETE FILES
769-
const supabase = await getStorage(project);
770-
const bucket = getBucket(project);
824+
// DELETE: Only for internal Infraa
825+
if (!isExternalStorage(project)) {
826+
const supabase = await getStorage(project);
827+
const bucket = getBucket(project);
771828

772-
let hasMoreFiles = true;
829+
let hasMoreFiles = true;
773830

774-
while (hasMoreFiles) {
775-
const { data: files, error } = await supabase.storage
776-
.from(bucket)
777-
.list(projectId, { limit: 100 });
831+
while (hasMoreFiles) {
832+
const { data: files, error } = await supabase.storage
833+
.from(bucket)
834+
.list(projectId, { limit: 100 });
778835

779-
if (error) throw error;
836+
if (error) throw error;
780837

781-
if (files && files.length > 0) {
782-
const paths = files.map(f => `${projectId}/${f.name}`);
783-
await supabase.storage.from(bucket).remove(paths);
784-
} else {
785-
hasMoreFiles = false;
838+
if (files && files.length > 0) {
839+
const paths = files.map(f => `${projectId}/${f.name}`);
840+
await supabase.storage.from(bucket).remove(paths);
841+
} else {
842+
hasMoreFiles = false;
843+
}
786844
}
787845
}
788846

@@ -800,7 +858,8 @@ module.exports.deleteProject = async (req, res) => {
800858
module.exports.analytics = async (req, res) => {
801859
try {
802860
const { projectId } = req.params;
803-
const project = await Project.findOne({ _id: projectId });
861+
const project = await Project.findOne({ _id: projectId, owner: req.user._id });
862+
if (!project) return res.status(404).json({ error: "Project not found or access denied." });
804863
const totalRequests = await Log.countDocuments({ projectId });
805864
const logs = await Log.find({ projectId }).sort({ timestamp: -1 }).limit(50);
806865

@@ -828,4 +887,71 @@ module.exports.analytics = async (req, res) => {
828887
} catch (err) {
829888
res.status(500).json({ error: err.message });
830889
}
890+
}
891+
892+
// FUNCTION - TOGGLE AUTH
893+
module.exports.toggleAuth = async (req, res) => {
894+
try {
895+
const { projectId } = req.params;
896+
const { enable } = req.body; // true or false
897+
898+
// Ensure user owns project
899+
const project = await Project.findOne({ _id: projectId, owner: req.user._id });
900+
if (!project) return res.status(404).json({ error: "Project not found" });
901+
902+
if (enable) {
903+
let usersCol = project.collections.find(c => c.name === 'users');
904+
if (!usersCol) {
905+
usersCol = {
906+
name: 'users',
907+
model: [
908+
{ key: 'email', type: 'String', required: true },
909+
{ key: 'username', type: 'String', required: false },
910+
{ key: 'password', type: 'String', required: true },
911+
{ key: 'emailVerified', type: 'Boolean', required: false }
912+
]
913+
};
914+
project.collections.push(usersCol);
915+
} else {
916+
if (!validateUsersSchema(usersCol.model)) {
917+
return res.status(422).json({
918+
error: "Invalid Users Schema",
919+
message: "The 'users' collection must have required 'email' and 'password' string fields. Please fix the schema before enabling Auth."
920+
});
921+
}
922+
}
923+
}
924+
925+
project.isAuthEnabled = !!enable;
926+
await project.save();
927+
928+
await deleteProjectById(projectId);
929+
await deleteProjectByApiKeyCache(project.publishableKey);
930+
await deleteProjectByApiKeyCache(project.secretKey);
931+
932+
const projectObj = project.toObject();
933+
delete projectObj.publishableKey;
934+
delete projectObj.secretKey;
935+
delete projectObj.jwtSecret;
936+
937+
if (projectObj.collections && Array.isArray(projectObj.collections)) {
938+
projectObj.collections = projectObj.collections.map(col => {
939+
if (col.name === 'users' && col.model) {
940+
return {
941+
...col,
942+
model: col.model.filter(m => m.key !== 'password')
943+
};
944+
}
945+
return col;
946+
});
947+
}
948+
949+
res.json({
950+
message: `Authentication ${project.isAuthEnabled ? 'enabled' : 'disabled'} successfully`,
951+
isAuthEnabled: project.isAuthEnabled,
952+
project: projectObj
953+
});
954+
} catch (err) {
955+
res.status(500).json({ error: err.message });
956+
}
831957
}

0 commit comments

Comments
 (0)