From 450fc96a1fef0cd56ba6893cdfd40e67e20333b9 Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Fri, 6 Mar 2026 17:46:37 +0530 Subject: [PATCH 01/10] feat: implement major auth system upgrade with dynamic forms and enhanced security --- backend/app.js | 1 + backend/controllers/project.controller.js | 124 +++-- backend/controllers/userAuth.controller.js | 314 +++++++++++- backend/middleware/checkAuthEnabled.js | 25 + backend/middleware/verifyApiKey.js | 2 + backend/models/Project.js | 1 + backend/queues/authEmailQueue.js | 34 ++ backend/queues/emailQueue.js | 8 +- backend/routes/projects.js | 14 +- backend/routes/userAuth.js | 20 +- backend/utils/emailService.js | 67 ++- backend/utils/network.js | 3 - frontend/src/components/DatabaseSidebar.jsx | 8 +- frontend/src/pages/Auth.jsx | 525 +++++++++++++++++--- frontend/src/pages/CreateCollection.jsx | 31 +- frontend/src/pages/ProjectDetails.jsx | 4 +- 16 files changed, 1037 insertions(+), 144 deletions(-) create mode 100644 backend/middleware/checkAuthEnabled.js create mode 100644 backend/queues/authEmailQueue.js diff --git a/backend/app.js b/backend/app.js index 361082fd7..39e385cc2 100644 --- a/backend/app.js +++ b/backend/app.js @@ -18,6 +18,7 @@ const { getPublicIp } = require('./utils/network'); // Initialize Queue Workers require('./queues/emailQueue'); +require('./queues/authEmailQueue'); // Middleware (We apply admin options globally except where overridden via dynamic handling) app.use(express.json()); diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 6e8bbbed1..2e5cfbeaa 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -95,20 +95,33 @@ 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); } - 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; + }); + } + res.json(projectObj); } catch (err) { res.status(500).json({ error: err.message }); @@ -371,7 +384,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'); + } + + const features = new QueryEngine(query, req.query) .filter() .sort() .paginate(); @@ -391,6 +409,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." }); + } + const finalCollectionName = `${project._id}_${collectionName}`; const incomingData = req.body; @@ -480,6 +502,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]; + }); + } + const docToEdit = await Model.findById(id); if (!docToEdit) { return res.status(404).json({ error: "Document not found." }); @@ -505,8 +535,12 @@ module.exports.editRow = async (req, res) => { } const updatedDoc = await docToEdit.save(); + const responseData = updatedDoc.toObject(); + if (collectionName === 'users') { + delete responseData.password; + } - 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 +788,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) { } } - 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; + } } } @@ -828,4 +868,28 @@ module.exports.analytics = async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } +} + +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" }); + + project.isAuthEnabled = !!enable; + + await project.save(); + await deleteProjectById(project._id); // Clear cache so frontend gets updated isAuthEnabled + + res.json({ + message: `Authentication ${project.isAuthEnabled ? 'enabled' : 'disabled'} successfully`, + isAuthEnabled: project.isAuthEnabled, + project: project + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } } \ No newline at end of file diff --git a/backend/controllers/userAuth.controller.js b/backend/controllers/userAuth.controller.js index 754cd1521..4ad800936 100644 --- a/backend/controllers/userAuth.controller.js +++ b/backend/controllers/userAuth.controller.js @@ -2,7 +2,10 @@ const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); const { z } = require('zod'); const mongoose = require('mongoose'); -const { loginSchema, userSignupSchema } = require('../utils/input.validation'); +const Project = require('../models/Project'); +const redis = require('../config/redis'); +const { authEmailQueue } = require('../queues/authEmailQueue'); +const { loginSchema, signupSchema, userSignupSchema, resetPasswordSchema, onlyEmailSchema, verifyOtpSchema, changePasswordSchema } = require('../utils/input.validation'); module.exports.signup = async (req, res) => { try { @@ -24,30 +27,47 @@ module.exports.signup = async (req, res) => { const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(password, salt); + // Generate OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const newUser = { username, email, password: hashedPassword, + emailVerified: false, ...otherData, createdAt: new Date() }; const result = await collection.insertOne(newUser); + // Save OTP to Redis (5 mins expiry) + await redis.set(`project:${project._id}:otp:verification:${email}`, otp, 'EX', 300); + + // Queue Email + await authEmailQueue.add('send-verification-email', { + email, + otp, + type: 'verification', + pname: project.name + }); + const token = jwt.sign( { userId: result.insertedId, projectId: project._id }, project.jwtSecret ); res.status(201).json({ - message: "User registered successfully", + message: "User registered successfully. Please verify your email.", token: token, userId: result.insertedId }); } catch (err) { - if (err instanceof z.ZodError) return res.status(400).json({ error: err.errors }); - res.status(500).json({ error: err.message }); // Fixed: .json() + if (err instanceof z.ZodError) { + return res.status(400).json({ error: err.issues?.[0]?.message || err.errors?.[0]?.message || "Validation failed" }); + } + res.status(500).json({ error: err.message }); console.log(err) } } @@ -110,4 +130,288 @@ module.exports.me = async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } -} \ No newline at end of file +} + +// ADMIN - Create User (from Dashboard) +module.exports.createAdminUser = async (req, res) => { + try { + const { projectId } = req.params; + 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 parsedData = userSignupSchema.parse(req.body); + const { email, password, username, ...otherData } = parsedData; + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const existingUser = await collection.findOne({ email }); + if (existingUser) { + return res.status(400).json({ error: "User already exists with this email." }); + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + const newUser = { + username, + email, + password: hashedPassword, + emailVerified: true, + ...otherData, + createdAt: new Date() + }; + + const result = await collection.insertOne(newUser); + + res.status(201).json({ + message: "User created successfully", + user: { _id: result.insertedId, email, username, createdAt: newUser.createdAt } + }); + + } catch (err) { + if (err instanceof z.ZodError) { + console.error(err); + return res.status(400).json({ error: err.issues?.[0]?.message || err.errors?.[0]?.message || "Validation failed" }); + } + res.status(500).json({ error: err.message }); + } +} + +// ADMIN - Reset User Password (from Dashboard) +module.exports.resetPassword = async (req, res) => { + try { + const { projectId, userId } = req.params; + 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 { newPassword } = req.body; + + if (!newPassword || newPassword.length < 6) { + return res.status(400).json({ error: "Password must be at least 6 characters" }); + } + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + const result = await collection.updateOne( + { _id: new mongoose.Types.ObjectId(userId) }, + { $set: { password: hashedPassword } } + ); + + if (result.matchedCount === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ message: "Password updated successfully" }); + + } catch (err) { + res.status(500).json({ error: err.message }); + } +} + +// USER PROFILE ENDPOINTS + +module.exports.verifyEmail = async (req, res) => { + try { + const project = req.project; + const { email, otp } = verifyOtpSchema.parse(req.body); + + const redisKey = `project:${project._id}:otp:verification:${email}`; + const storedOtp = await redis.get(redisKey); + + if (!storedOtp || storedOtp !== otp) { + return res.status(400).json({ error: "Invalid or expired OTP" }); + } + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const result = await collection.updateOne( + { email }, + { $set: { emailVerified: true } } + ); + + if (result.matchedCount === 0) return res.status(404).json({ error: "User not found" }); + + await redis.del(redisKey); + res.json({ message: "Email verified successfully" }); + + } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.issues?.[0]?.message || "Validation failed" }); + res.status(500).json({ error: err.message }); + } +}; + +module.exports.requestPasswordReset = async (req, res) => { + try { + const project = req.project; + const { email } = onlyEmailSchema.parse(req.body); + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const user = await collection.findOne({ email }); + if (!user) { + return res.json({ message: "If that email exists, a reset code has been sent." }); + } + + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + await redis.set(`project:${project._id}:otp:reset:${email}`, otp, 'EX', 300); + + await authEmailQueue.add('send-reset-email', { email, otp, type: 'password_reset', pname: project.name }); + + res.json({ message: "If that email exists, a reset code has been sent." }); + } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.issues?.[0]?.message || "Validation failed" }); + res.status(500).json({ error: err.message }); + } +}; + +module.exports.resetPasswordUser = async (req, res) => { + try { + const project = req.project; + const { email, otp, newPassword } = resetPasswordSchema.parse(req.body); + + const redisKey = `project:${project._id}:otp:reset:${email}`; + const storedOtp = await redis.get(redisKey); + + if (!storedOtp || storedOtp !== otp) { + return res.status(400).json({ error: "Invalid or expired OTP" }); + } + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const result = await collection.updateOne( + { email }, + { $set: { password: hashedPassword } } + ); + + if (result.matchedCount === 0) return res.status(404).json({ error: "User not found" }); + + await redis.del(redisKey); + res.json({ message: "Password updated successfully" }); + + } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.issues?.[0]?.message || "Validation failed" }); + res.status(500).json({ error: err.message }); + } +}; + +module.exports.updateProfile = async (req, res) => { + try { + const project = req.project; + + const tokenHeader = req.header('Authorization'); + if (!tokenHeader) return res.status(401).json({ error: "Access Denied: No Token Provided" }); + const token = tokenHeader.replace("Bearer ", ""); + const decoded = jwt.verify(token, project.jwtSecret); + + const username = req.body.username; + if (!username || username.length < 3) return res.status(400).json({ error: "Username must be at least 3 characters." }); + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const result = await collection.updateOne( + { _id: new mongoose.Types.ObjectId(decoded.userId) }, + { $set: { username } } + ); + + res.json({ message: "Profile updated successfully" }); + + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +module.exports.changePasswordUser = async (req, res) => { + try { + const project = req.project; + + const tokenHeader = req.header('Authorization'); + if (!tokenHeader) return res.status(401).json({ error: "Access Denied: No Token Provided" }); + const token = tokenHeader.replace("Bearer ", ""); + const decoded = jwt.verify(token, project.jwtSecret); + + const { currentPassword, newPassword } = changePasswordSchema.parse(req.body); + + const collectionName = `${project._id}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const user = await collection.findOne({ _id: new mongoose.Types.ObjectId(decoded.userId) }); + if (!user) return res.status(404).json({ error: "User not found" }); + + const validPass = await bcrypt.compare(currentPassword, user.password); + if (!validPass) return res.status(400).json({ error: "Invalid current password" }); + + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(newPassword, salt); + + await collection.updateOne( + { _id: new mongoose.Types.ObjectId(decoded.userId) }, + { $set: { password: hashedPassword } } + ); + + res.json({ message: "Password changed successfully" }); + + } catch (err) { + if (err instanceof z.ZodError) return res.status(400).json({ error: err.issues?.[0]?.message || "Validation failed" }); + res.status(500).json({ error: err.message }); + } +}; + +// ADMIN: GET USER DETAILS +module.exports.getUserDetails = async (req, res) => { + try { + const { projectId, userId } = req.params; + + const collectionName = `${projectId}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const user = await collection.findOne({ _id: new mongoose.Types.ObjectId(userId) }); + if (!user) return res.status(404).json({ error: "User not found" }); + + // Remove password hash before returning + const { password, ...safeUser } = user; + + res.json(safeUser); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +// ADMIN: UPDATE CUSTOM USER FIELDS +module.exports.updateAdminUser = async (req, res) => { + try { + const { projectId, userId } = req.params; + const updateData = req.body; + + // Prevent admin from inadvertently changing password hash directly from this endpoint + delete updateData.password; + delete updateData._id; + + const collectionName = `${projectId}_users`; + const collection = mongoose.connection.db.collection(collectionName); + + const result = await collection.updateOne( + { _id: new mongoose.Types.ObjectId(userId) }, + { $set: updateData } + ); + + if (result.matchedCount === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ message: "User updated successfully" }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; \ No newline at end of file diff --git a/backend/middleware/checkAuthEnabled.js b/backend/middleware/checkAuthEnabled.js new file mode 100644 index 000000000..0eac55765 --- /dev/null +++ b/backend/middleware/checkAuthEnabled.js @@ -0,0 +1,25 @@ +module.exports = (req, res, next) => { + const project = req.project; + + if (!project.isAuthEnabled) { + return res.status(403).json({ + error: "Authentication service is disabled", + message: "Please enable Auth in the urBackend dashboard for this project to use this endpoint." + }); + } + + // Check if a 'users' collection schema has been defined by the developer + const usersCollection = project.collections?.find(c => c.name === 'users'); + + if (!usersCollection) { + return res.status(403).json({ + error: "User Schema Missing", + message: "Authentication is enabled, but the 'users' collection schema has not been defined. Please create a 'users' collection in the dashboard to define your custom user fields." + }); + } + + // Attach the usersSchema to the request so controllers can use it for dynamic validation + req.usersSchema = usersCollection.model; + + next(); +}; diff --git a/backend/middleware/verifyApiKey.js b/backend/middleware/verifyApiKey.js index 8ef6ee1e7..ef3d8694d 100644 --- a/backend/middleware/verifyApiKey.js +++ b/backend/middleware/verifyApiKey.js @@ -21,6 +21,7 @@ module.exports = async (req, res, next) => { if (!project) { project = await Project.findOne({ [keyField]: hashedApi }) .select(` + name owner resources collections @@ -30,6 +31,7 @@ module.exports = async (req, res, next) => { storageUsed jwtSecret allowedDomains + isAuthEnabled `) .populate('owner', 'isVerified') .lean(); diff --git a/backend/models/Project.js b/backend/models/Project.js index 1f894bc43..4afd7a2c7 100644 --- a/backend/models/Project.js +++ b/backend/models/Project.js @@ -49,6 +49,7 @@ const projectSchema = new mongoose.Schema({ type: String, required: true }, + isAuthEnabled: { type: Boolean, default: false }, collections: [collectionSchema], allowedDomains: { diff --git a/backend/queues/authEmailQueue.js b/backend/queues/authEmailQueue.js new file mode 100644 index 000000000..7369ec3d6 --- /dev/null +++ b/backend/queues/authEmailQueue.js @@ -0,0 +1,34 @@ +const { Queue, Worker } = require('bullmq'); +const connection = require('../config/redis'); +const { sendAuthOtpEmail } = require('../utils/emailService'); + +// Create the email queue specifically for fast OTPs +const authEmailQueue = new Queue('auth-email-queue', { connection }); + +// Initialize Worker with Rate Limiting +const worker = new Worker('auth-email-queue', async (job) => { + const { email, otp, type, pname } = job.data; + try { + console.log(`[Queue] Processing ${type} email for: ${email}`); + await sendAuthOtpEmail(email, { otp, type, pname}); + } catch (error) { + console.error(`[Queue] Failed to send auth email to ${email}:`, error); + throw error; + } +}, { + connection, + limiter: { + max: 2, + duration: 1000, + } +}); + +worker.on('completed', (job) => { + console.log(`[Queue] Job ${job.id} completed successfully`); +}); + +worker.on('failed', (job, err) => { + console.error(`[Queue] Job ${job.id} failed:`, err); +}); + +module.exports = { authEmailQueue }; diff --git a/backend/queues/emailQueue.js b/backend/queues/emailQueue.js index 09318f1d1..f4f202b9c 100644 --- a/backend/queues/emailQueue.js +++ b/backend/queues/emailQueue.js @@ -1,13 +1,7 @@ const { Queue, Worker } = require('bullmq'); -const IORedis = require('ioredis'); +const connection = require('../config/redis'); const { sendReleaseEmail } = require('../utils/emailService'); -const connection = new IORedis({ - host: process.env.REDIS_HOST || '127.0.0.1', - port: process.env.REDIS_PORT || 6379, - maxRetriesPerRequest: null, -}); - // Create the email queue const emailQueue = new Queue('email-queue', { connection }); diff --git a/backend/routes/projects.js b/backend/routes/projects.js index ad91f08a7..5b7c529b6 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -26,9 +26,12 @@ const { deleteExternalDbConfig, deleteExternalStorageConfig, analytics, - updateAllowedDomains + updateAllowedDomains, + toggleAuth } = require("../controllers/project.controller") +const { createAdminUser, resetPassword, getUserDetails, updateAdminUser } = require('../controllers/userAuth.controller'); + const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 } }); // 10MB Limit @@ -98,4 +101,13 @@ router.delete('/:projectId/storage/files', authMiddleware, deleteAllFiles); // ANALYTICS router.get('/:projectId/analytics', authMiddleware, analytics); +// TOGGLE AUTH +router.patch('/:projectId/auth/toggle', authMiddleware, verifyEmail, toggleAuth); + +// ADMIN AUTH +router.post('/:projectId/admin/users', authMiddleware, createAdminUser); +router.patch('/:projectId/admin/users/:userId/password', authMiddleware, resetPassword); +router.get('/:projectId/admin/users/:userId', authMiddleware, getUserDetails); +router.put('/:projectId/admin/users/:userId', authMiddleware, updateAdminUser); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/userAuth.js b/backend/routes/userAuth.js index 964523bbb..157fe56bd 100644 --- a/backend/routes/userAuth.js +++ b/backend/routes/userAuth.js @@ -1,15 +1,27 @@ const express = require('express'); const router = express.Router(); const verifyApiKey = require('../middleware/verifyApiKey'); -const { signup, login, me } = require('../controllers/userAuth.controller'); +const checkAuthEnabled = require('../middleware/checkAuthEnabled'); +const { signup, login, me, verifyEmail, requestPasswordReset, resetPasswordUser, updateProfile, changePasswordUser } = require('../controllers/userAuth.controller'); // SIGNUP ROUTE -router.post('/signup', verifyApiKey, signup); +router.post('/signup', verifyApiKey, checkAuthEnabled, signup); // LOGIN ROUTE -router.post('/login', verifyApiKey, login); +router.post('/login', verifyApiKey, checkAuthEnabled, login); // GET CURRENT USER -router.get('/me', verifyApiKey, me); +router.get('/me', verifyApiKey, checkAuthEnabled, me); + +// EMAIL VERIFICATION +router.post('/verify-email', verifyApiKey, checkAuthEnabled, verifyEmail); + +// PASSWORD RESET +router.post('/request-password-reset', verifyApiKey, checkAuthEnabled, requestPasswordReset); +router.post('/reset-password', verifyApiKey, checkAuthEnabled, resetPasswordUser); + +// PROFILE MANAGEMENT +router.put('/update-profile', verifyApiKey, checkAuthEnabled, updateProfile); +router.put('/change-password', verifyApiKey, checkAuthEnabled, changePasswordUser); module.exports = router; \ No newline at end of file diff --git a/backend/utils/emailService.js b/backend/utils/emailService.js index 8ebbb0afc..e0653d135 100644 --- a/backend/utils/emailService.js +++ b/backend/utils/emailService.js @@ -126,4 +126,69 @@ async function sendReleaseEmail(email, { version, title, content }) { } } -module.exports = { sendOtp, sendReleaseEmail }; \ No newline at end of file +async function sendAuthOtpEmail(email, { otp, type, pname }) { + pname = pname || "urBackend"; + const isVerify = type === 'verification'; + const subject = isVerify ? "Verify your account" : "Reset your password"; + const header = isVerify ? "Verify your email address" : "Reset your password"; + const desc = isVerify + ? "Use the following code to complete your verification process." + : "Use the following code to reset your password."; + + try { + const htmlContent = ` + + + + + + +
+ +

${header}

+
+ ${desc} This code will expire in 5 minutes. +
+
${otp}
+
+ If you didn't request this code, you can safely ignore this email. +
+ +
+ + + `; + + const safeEmailHandle = pname.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + + const { data, error } = await resend.emails.send({ + from: `${pname} <${safeEmailHandle}.urbackend@apps.bitbros.in>`, + to: email, + subject: subject, + html: htmlContent, + replyTo: `${pname} <${safeEmailHandle}.urbackend@apps.bitbros.in>`, + }); + + if (error) { + console.error("[Resend Error]", error); + throw new Error(error.message || "Failed to send email"); + } + return { data }; + } catch (error) { + console.error("[Auth Queue Email Service Error]", error); + throw error; + } +} + +module.exports = { sendOtp, sendReleaseEmail, sendAuthOtpEmail }; \ No newline at end of file diff --git a/backend/utils/network.js b/backend/utils/network.js index 79266be76..b57637eb1 100644 --- a/backend/utils/network.js +++ b/backend/utils/network.js @@ -1,10 +1,7 @@ -// Node 18+ has global fetch, so no import needed - let cachedIp = null; let lastFetch = 0; async function getPublicIp() { - // Cache IP for 1 hour to avoid spamming the IP service const now = Date.now(); if (cachedIp && (now - lastFetch < 3600000)) { return cachedIp; diff --git a/frontend/src/components/DatabaseSidebar.jsx b/frontend/src/components/DatabaseSidebar.jsx index 24352c45d..0bf19d67c 100644 --- a/frontend/src/components/DatabaseSidebar.jsx +++ b/frontend/src/components/DatabaseSidebar.jsx @@ -17,12 +17,14 @@ export default function DatabaseSidebar({ projectId, onRequestDelete }) { + const visibleCollections = collections.filter(c => c.name !== 'users' || activeCollection?.name === 'users'); + return (