-
Notifications
You must be signed in to change notification settings - Fork 68
feat: implement major auth system upgrade with dynamic forms and enha… #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
450fc96
126a878
5097786
d5a1ed6
e4feba0
0b11a64
be3ea62
4978c6b
7b91f98
8ed4f89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,16 @@ const { getCompiledModel } = require("../utils/injectModel") | |
| const QueryEngine = require("../utils/queryEngine"); | ||
| const { storageRegistry } = require("../utils/registry"); | ||
| const { deleteProjectByApiKeyCache, setProjectById, getProjectById, deleteProjectById } = require("../services/redisCaching"); | ||
| const { v4: uuidv4 } = require('uuid'); | ||
| const { getPublicIp } = require("../utils/network"); | ||
|
|
||
| const validateUsersSchema = (schema) => { | ||
| if (!Array.isArray(schema)) return false; | ||
| const hasEmail = schema.find(f => f.key === 'email' && f.type === 'String' && f.required); | ||
| const hasPassword = schema.find(f => f.key === 'password' && f.type === 'String' && f.required); | ||
| return !!(hasEmail && hasPassword); | ||
| }; | ||
|
|
||
|
|
||
|
|
||
| const getBucket = (project) => | ||
|
|
@@ -95,20 +103,38 @@ module.exports.getAllProject = async (req, res) => { | |
|
|
||
| module.exports.getSingleProject = async (req, res) => { | ||
| try { | ||
| let project; | ||
| project = await getProjectById(req.params.projectId); | ||
| let projectObj; | ||
| if (!project) { | ||
| project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-publishableKey -secretKey -jwtSecret'); | ||
| let projectObj = await getProjectById(req.params.projectId); | ||
|
|
||
| if (!projectObj) { | ||
| const project = await Project.findOne({ _id: req.params.projectId, owner: req.user._id }).select('-publishableKey -secretKey -jwtSecret'); | ||
| if (!project) return res.status(404).json({ error: "Project not found." }); | ||
| projectObj = project.toObject(); | ||
| await setProjectById(req.params.projectId, project); | ||
| await setProjectById(req.params.projectId, projectObj); | ||
| } | ||
|
|
||
| // Ownership Check (Even for Cache) | ||
| if (projectObj.owner.toString() !== req.user._id.toString()) { | ||
| return res.status(403).json({ error: "Access denied." }); | ||
| } | ||
|
|
||
| projectObj = project; | ||
| // Just to be safe, we remove sensitive fields again even if from cache | ||
| delete projectObj.publishableKey; | ||
| delete projectObj.secretKey; | ||
| delete projectObj.jwtSecret; | ||
|
|
||
| // SANITIZE COLLECTION MODELS: Remove "password" from "users" collection schema | ||
| if (projectObj.collections && Array.isArray(projectObj.collections)) { | ||
| projectObj.collections = projectObj.collections.map(col => { | ||
| if (col.name === 'users' && col.model) { | ||
| return { | ||
| ...col, | ||
| model: col.model.filter(m => m.key !== 'password') | ||
| }; | ||
| } | ||
| return col; | ||
| }); | ||
| } | ||
|
Comment on lines
+126
to
+136
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| res.json(projectObj); | ||
| } catch (err) { | ||
| res.status(500).json({ error: err.message }); | ||
|
|
@@ -283,6 +309,7 @@ module.exports.deleteExternalStorageConfig = async (req, res) => { | |
|
|
||
|
|
||
|
|
||
| // POST REQ FOR CREATE COLLECTION | ||
| module.exports.createCollection = async (req, res) => { | ||
| try { | ||
| const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body); | ||
|
|
@@ -295,14 +322,19 @@ module.exports.createCollection = async (req, res) => { | |
|
|
||
| if (!project.jwtSecret) project.jwtSecret = uuidv4(); | ||
|
|
||
| if (collectionName === 'users') { | ||
| if (!validateUsersSchema(schema)) { | ||
| return res.status(422).json({ error: "The 'users' collection must have required 'email' and 'password' string fields." }); | ||
| } | ||
| } | ||
|
|
||
| project.collections.push({ name: collectionName, model: schema }); | ||
| await project.save(); | ||
|
|
||
| await deleteProjectById(projectId); | ||
| await setProjectById(projectId, project); | ||
| await deleteProjectByApiKeyCache(project.publishableKey); | ||
| await deleteProjectByApiKeyCache(project.secretKey); | ||
| // RESPONSE | ||
| const projectObj = project.toObject(); | ||
| delete projectObj.publishableKey; | ||
| delete projectObj.secretKey; | ||
|
|
@@ -371,7 +403,12 @@ module.exports.getData = async (req, res) => { | |
|
|
||
| // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); | ||
|
|
||
| const features = new QueryEngine(model.find(), req.query) | ||
| const query = model.find(); | ||
| if (collectionName === 'users') { | ||
| query.select('-password'); | ||
| } | ||
|
Comment on lines
+406
to
+409
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const features = new QueryEngine(query, req.query) | ||
| .filter() | ||
| .sort() | ||
| .paginate(); | ||
|
|
@@ -391,6 +428,10 @@ module.exports.insertData = async (req, res) => { | |
| const project = await Project.findOne({ _id: projectId, owner: req.user._id }); | ||
| if (!project) return res.status(404).json({ error: "Project not found." }); | ||
|
|
||
| if (collectionName === 'users') { | ||
| return res.status(400).json({ error: "Direct inserts into 'users' collection are not allowed. Please use the Auth signup or admin endpoints." }); | ||
| } | ||
|
Comment on lines
+431
to
+433
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const finalCollectionName = `${project._id}_${collectionName}`; | ||
| const incomingData = req.body; | ||
|
|
||
|
|
@@ -419,7 +460,6 @@ module.exports.insertData = async (req, res) => { | |
| project.databaseUsed = (project.databaseUsed || 0) + docSize; | ||
| } | ||
| await project.save(); | ||
| await project.save(); | ||
|
|
||
| res.json(result); | ||
| } catch (err) { | ||
|
|
@@ -480,6 +520,14 @@ module.exports.editRow = async (req, res) => { | |
| const connection = await getConnection(projectId); | ||
| const Model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal); | ||
|
|
||
| if (collectionName === 'users') { | ||
| delete req.body.password; | ||
| // Also ensure it's not and nested or sneaky | ||
| Object.keys(req.body).forEach(key => { | ||
| if (key.toLowerCase().includes('password')) delete req.body[key]; | ||
| }); | ||
|
Comment on lines
+523
to
+528
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| const docToEdit = await Model.findById(id); | ||
| if (!docToEdit) { | ||
| return res.status(404).json({ error: "Document not found." }); | ||
|
|
@@ -505,8 +553,12 @@ module.exports.editRow = async (req, res) => { | |
| } | ||
|
|
||
| const updatedDoc = await docToEdit.save(); | ||
| const responseData = updatedDoc.toObject(); | ||
| if (collectionName === 'users') { | ||
| delete responseData.password; | ||
| } | ||
|
Comment on lines
+556
to
+559
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| res.json({ success: true, message: "Document edited successfully", data: updatedDoc }); | ||
| res.json({ success: true, message: "Document edited successfully", data: responseData }); | ||
|
|
||
| } catch (err) { | ||
| console.error("Edit Error:", err); | ||
|
|
@@ -754,35 +806,41 @@ module.exports.deleteProject = async (req, res) => { | |
| if (!project) { | ||
| return res.status(404).json({ error: "Project not found or access denied." }); | ||
| } | ||
| for (const col of project.collections) { | ||
| const collectionName = `${project._id}_${col.name}`; | ||
|
|
||
| // DROP COLLECTIONS: Only for internal databases | ||
| if (!project.resources.db.isExternal) { | ||
| for (const col of project.collections) { | ||
| const collectionName = `${project._id}_${col.name}`; | ||
| try { | ||
| await mongoose.connection.db.dropCollection(collectionName); | ||
| } catch (e) { } | ||
| } | ||
|
|
||
| try { | ||
| await mongoose.connection.db.dropCollection(collectionName); | ||
| await mongoose.connection.db.dropCollection(`${project._id}_users`); | ||
| } catch (e) { } | ||
| } | ||
|
Comment on lines
+810
to
822
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| try { | ||
| await mongoose.connection.db.dropCollection(`${project._id}_users`); | ||
| } catch (e) { } | ||
|
|
||
| // DELETE FILES | ||
| const supabase = await getStorage(project); | ||
| const bucket = getBucket(project); | ||
| // DELETE: Only for internal Infraa | ||
| if (!isExternalStorage(project)) { | ||
| const supabase = await getStorage(project); | ||
| const bucket = getBucket(project); | ||
|
|
||
| let hasMoreFiles = true; | ||
| let hasMoreFiles = true; | ||
|
|
||
| while (hasMoreFiles) { | ||
| const { data: files, error } = await supabase.storage | ||
| .from(bucket) | ||
| .list(projectId, { limit: 100 }); | ||
| while (hasMoreFiles) { | ||
| const { data: files, error } = await supabase.storage | ||
| .from(bucket) | ||
| .list(projectId, { limit: 100 }); | ||
|
|
||
| if (error) throw error; | ||
| if (error) throw error; | ||
|
|
||
| if (files && files.length > 0) { | ||
| const paths = files.map(f => `${projectId}/${f.name}`); | ||
| await supabase.storage.from(bucket).remove(paths); | ||
| } else { | ||
| hasMoreFiles = false; | ||
| if (files && files.length > 0) { | ||
| const paths = files.map(f => `${projectId}/${f.name}`); | ||
| await supabase.storage.from(bucket).remove(paths); | ||
| } else { | ||
| hasMoreFiles = false; | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+825
to
845
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
|
|
@@ -800,7 +858,8 @@ module.exports.deleteProject = async (req, res) => { | |
| module.exports.analytics = async (req, res) => { | ||
| try { | ||
| const { projectId } = req.params; | ||
| const project = await Project.findOne({ _id: projectId }); | ||
| const project = await Project.findOne({ _id: projectId, owner: req.user._id }); | ||
| if (!project) return res.status(404).json({ error: "Project not found or access denied." }); | ||
| const totalRequests = await Log.countDocuments({ projectId }); | ||
| const logs = await Log.find({ projectId }).sort({ timestamp: -1 }).limit(50); | ||
|
|
||
|
|
@@ -828,4 +887,71 @@ module.exports.analytics = async (req, res) => { | |
| } catch (err) { | ||
| res.status(500).json({ error: err.message }); | ||
| } | ||
| } | ||
|
|
||
| // FUNCTION - TOGGLE AUTH | ||
| module.exports.toggleAuth = async (req, res) => { | ||
| try { | ||
| const { projectId } = req.params; | ||
| const { enable } = req.body; // true or false | ||
|
|
||
| // Ensure user owns project | ||
| const project = await Project.findOne({ _id: projectId, owner: req.user._id }); | ||
| if (!project) return res.status(404).json({ error: "Project not found" }); | ||
|
|
||
| if (enable) { | ||
| let usersCol = project.collections.find(c => c.name === 'users'); | ||
| if (!usersCol) { | ||
| usersCol = { | ||
| name: 'users', | ||
| model: [ | ||
| { key: 'email', type: 'String', required: true }, | ||
| { key: 'username', type: 'String', required: false }, | ||
| { key: 'password', type: 'String', required: true }, | ||
| { key: 'emailVerified', type: 'Boolean', required: false } | ||
| ] | ||
| }; | ||
| project.collections.push(usersCol); | ||
| } else { | ||
| if (!validateUsersSchema(usersCol.model)) { | ||
| return res.status(422).json({ | ||
| error: "Invalid Users Schema", | ||
| message: "The 'users' collection must have required 'email' and 'password' string fields. Please fix the schema before enabling Auth." | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| project.isAuthEnabled = !!enable; | ||
| await project.save(); | ||
|
|
||
| await deleteProjectById(projectId); | ||
| await deleteProjectByApiKeyCache(project.publishableKey); | ||
| await deleteProjectByApiKeyCache(project.secretKey); | ||
|
|
||
| const projectObj = project.toObject(); | ||
| delete projectObj.publishableKey; | ||
| delete projectObj.secretKey; | ||
| delete projectObj.jwtSecret; | ||
|
|
||
| if (projectObj.collections && Array.isArray(projectObj.collections)) { | ||
| projectObj.collections = projectObj.collections.map(col => { | ||
| if (col.name === 'users' && col.model) { | ||
| return { | ||
| ...col, | ||
| model: col.model.filter(m => m.key !== 'password') | ||
| }; | ||
| } | ||
| return col; | ||
| }); | ||
| } | ||
|
|
||
| res.json({ | ||
| message: `Authentication ${project.isAuthEnabled ? 'enabled' : 'disabled'} successfully`, | ||
| isAuthEnabled: project.isAuthEnabled, | ||
| project: projectObj | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } catch (err) { | ||
| res.status(500).json({ error: err.message }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
setProjectByIdfunction is called withprojectObjwhich is the result ofproject.toObject(). This is good as it ensures a plain JavaScript object is cached, preventing potential issues with caching Mongoose documents directly. This aligns with the PR description's mention of fixing a critical caching bug.