diff --git a/backend/.gitignore b/backend/.gitignore index 1dcef2d9f..f95ba41c3 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ node_modules -.env \ No newline at end of file +.env +notes.txt \ No newline at end of file diff --git a/backend/app.js b/backend/app.js index 1cef5c7af..308da749f 100644 --- a/backend/app.js +++ b/backend/app.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose') const cors = require('cors') const dotenv = require('dotenv'); const app = express(); +const GC = require('./utils/GC'); dotenv.config(); // Middleware @@ -53,16 +54,22 @@ app.get('/', (req, res) => { res.status(200).json({ status: "success", message: "urBackend API is running ЁЯЪА" }) }); -// ЁЯЫбя╕П FAULT TOLERANCE: Global Error Handler +// FAULT TOLERANCE: Global Error Handler app.use((err, req, res, next) => { + if (err instanceof SyntaxError && err.status === 400 && 'body' in err) { + return res.status(400).json({ + error: "Invalid JSON format", + message: "Check your request body syntax. Stray characters outside the JSON object are not allowed." + }); + } + console.error("ЁЯФе Unhandled Error:", err.stack); res.status(500).json({ error: "Something went wrong!", message: err.message }); }); - -// ЁЯЫбя╕П DB CONNECTION & SERVER START +// DB CONNECTION & SERVER START // (Only connect if NOT in Test Mode) if (process.env.NODE_ENV !== 'test') { diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js index 162e87054..ddc064be4 100644 --- a/backend/controllers/data.controller.js +++ b/backend/controllers/data.controller.js @@ -1,41 +1,31 @@ -const { sanitize } = require("../utils/input.validation") -const { ObjectId } = require('mongodb'); +const { sanitize } = require("../utils/input.validation"); const mongoose = require('mongoose'); const Project = require("../models/Project"); +const { getConnection } = require("../utils/connectionManager"); +const { getCompiledModel } = require("../utils/injectModel"); +// 1. INSERT DATA module.exports.insertData = async (req, res) => { try { const { collectionName } = req.params; - const project = req.project; - - // Latest Project Data fetch karein (taaki accurate usage mile) - const currentProject = await Project.findById(project._id); + const project = req.project; // Middleware se mila project metadata - const collectionConfig = currentProject.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); const schemaRules = collectionConfig.model; const incomingData = req.body; const cleanData = {}; - // --- VALIDATION & SANITIZATION --- + // Validation & Sanitization for (const field of schemaRules) { if (field.required && incomingData[field.key] === undefined) { - return res.status(400).json({ - error: `Field '${field.key}' is required.`, - field: field.key - }); + return res.status(400).json({ error: `Field '${field.key}' is required.` }); } if (incomingData[field.key] !== undefined) { - // Type checking logic (same as before)... + // Type Checks if (field.type === 'Number' && typeof incomingData[field.key] !== 'number') return res.status(400).send(`Field '${field.key}' must be a Number.`); if (field.type === 'Boolean' && typeof incomingData[field.key] !== 'boolean') return res.status(400).send(`Field '${field.key}' must be a Boolean.`); - cleanData[field.key] = incomingData[field.key]; } } @@ -43,194 +33,134 @@ module.exports.insertData = async (req, res) => { const safeData = sanitize(cleanData); Object.assign(cleanData, safeData); - // CHECK DB LIMIT --- - // Calculate approx size in bytes - const docSize = Buffer.byteLength(JSON.stringify(cleanData)); - - if (currentProject.databaseUsed + docSize > currentProject.databaseLimit) { - return res.status(403).send("Database limit exceeded. Delete some data to free up space."); + // Usage Limit Check (Internal Only) + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(cleanData)); + if ((project.databaseUsed || 0) + docSize > project.databaseLimit) { + return res.status(403).send("Database limit exceeded."); + } } - // Insert Data - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); - const result = await collection.insertOne(cleanData); + // Get Connection & Model + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); - // UPDATE USAGE --- - currentProject.databaseUsed += docSize; - await currentProject.save(); + const result = await Model.create(cleanData); - res.status(201).json({ - message: "Data inserted successfully", - insertedId: result.insertedId, - data: cleanData - }); + // Update Project Metadata (Internal Only) + if (!project.isExternal) { + project.databaseUsed = (project.databaseUsed || 0) + docSize; + await project.save(); + } + res.status(201).json(result); } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + console.error("Insert Error:", err); + res.status(500).json({ error: err.message }); } -} - +}; +// 2. GET ALL DATA module.exports.getAllData = async (req, res) => { try { const { collectionName } = req.params; const project = req.project; const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - const finalCollectionName = `${project._id}_${collectionName}`; + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - const data = await mongoose.connection.db - .collection(finalCollectionName) - .find({}) - .toArray(); + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + const data = await Model.find({}).limit(100); res.json(data); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + console.log("error----------------") + res.status(500).json({ error: err.message }); } -} +}; +// 3. GET SINGLE DOC module.exports.getSingleDoc = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - // Security Check const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - - const finalCollectionName = `${project._id}_${collectionName}`; + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - // Find document by _id - const doc = await mongoose.connection.db - .collection(finalCollectionName) - .findOne({ _id: new ObjectId(id) }); // ID Conversion is important + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); - if (!doc) { - return res.status(404).send("Document not found."); - } + const doc = await Model.findById(id); + if (!doc) return res.status(404).send("Document not found."); res.json(doc); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} - - - +}; +// 4. UPDATE DATA module.exports.updateSingleData = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - const incomingData = req.body; // Naya data + const incomingData = req.body; - // Security & Config Find const collectionConfig = project.collections.find(c => c.name === collectionName); - if (!collectionConfig) { - return res.status(404).json({ - error: "Collection not found", - collection: collectionName - }); - } - // --- VALIDATION REPEAT (Important) --- + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); + + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + + // Basic Schema Validation const schemaRules = collectionConfig.model; for (const key in incomingData) { - // 1. Check if field exists in schema const fieldRule = schemaRules.find(f => f.key === key); - if (!fieldRule) { - - // Not Found - return res.status(400).send(`Field '${key}' is not defined in the schema.`); - } - - // Check Type - if (fieldRule.type === 'Number' && typeof incomingData[key] !== 'number') { - return res.status(400).send(`Field '${key}' must be a Number.`); - } - if (fieldRule.type === 'Boolean' && typeof incomingData[key] !== 'boolean') { - return res.status(400).send(`Field '${key}' must be a Boolean.`); - } - // String check is loose in JS, usually fine. + if (!fieldRule) return res.status(400).send(`Field '${key}' not in schema.`); + // Type checks... (Number, Boolean etc.) } + const result = await Model.findByIdAndUpdate(id, { $set: incomingData }, { new: true }); + if (!result) return res.status(404).send("Document not found."); - const safeData = sanitize(incomingData); - incomingData = safeData; - - - const finalCollectionName = `${project._id}_${collectionName}`; - - const result = await mongoose.connection.db - .collection(finalCollectionName) - .updateOne( - { _id: new ObjectId(id) }, - { $set: incomingData } - ); - - if (result.matchedCount === 0) { - return res.status(404).send("Document not found to update."); - } - - res.json({ message: "Document updated successfully", id, updatedFields: incomingData }); - + res.json({ message: "Updated", data: result }); } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} - - - +}; +// 5. DELETE DATA module.exports.deleteSingleDoc = async (req, res) => { try { const { collectionName, id } = req.params; const project = req.project; - // Latest Project Fetch - const currentProject = await Project.findById(project._id); - - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) return res.status(404).json({ error: "Collection not found" }); - // First find the document to know size - const docToDelete = await collection.findOne({ _id: new ObjectId(id) }); + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + const docToDelete = await Model.findById(id); if (!docToDelete) return res.status(404).send("Document not found."); - // Calculate size to subtract - // It also counts _id field - const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(docToDelete)); + } - // Delete - await collection.deleteOne({ _id: new ObjectId(id) }); + await Model.deleteOne({ _id: id }); - // Update Usage (Ensure it doesn't go below 0) - currentProject.databaseUsed = Math.max(0, currentProject.databaseUsed - docSize); - await currentProject.save(); + if (!project.isExternal) { + project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); + await project.save(); + } res.json({ message: "Document deleted", id }); - } catch (err) { - console.log(err) - res.status(500).json({ error: 'Some issue at our end' }); + res.status(500).json({ error: err.message }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 714d54b9d..15ccf6a16 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,9 +1,13 @@ const mongoose = require("mongoose") const Project = require("../models/Project") +const Log = require("../models/Log") const { createProjectSchema, createCollectionSchema } = require('../utils/input.validation'); const { generateApiKey, hashApiKey } = require('../utils/api'); const { z } = require('zod'); - +const { encrypt } = require('../utils/encryption'); +const { URL } = require('url'); +const { getConnection } = require("../utils/connectionManager"); +const { getCompiledModel } = require("../utils/injectModel") // CONFIGURATION @@ -87,8 +91,59 @@ module.exports.regenerateApiKey = async (req, res) => { } catch (err) { res.status(500).json({ error: err.message }); } +}; + + +//function to validate monguri +const isSafeUri = (uri) => { + try { + const parsed = new URL(uri); + const host = parsed.hostname.toLowerCase(); + const badHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1']; + return !badHosts.includes(host); + } catch (e) { return false; } +}; + + + +module.exports.updateExternalConfig = async (req, res) => { + try { + const { projectId } = req.params; + const { dbUri, storageUrl, storageKey, storageProvider } = req.body; + + if (!dbUri && (!storageUrl || !storageKey)) { + return res.status(400).json({ + error: "At least one configuration (Database or Storage) must be provided." + }); + } + + if (dbUri && !isSafeUri(dbUri)) return res.status(400).json({ error: "Invalid DB URI" }); + + const configToEncrypt = {}; + if (dbUri) configToEncrypt.dbUri = dbUri; + if (storageUrl && storageKey) { + configToEncrypt.storageUrl = storageUrl; + configToEncrypt.storageKey = storageKey; + configToEncrypt.storageProvider = storageProvider || 'supabase'; + } + + const encryptedConfig = encrypt(JSON.stringify(configToEncrypt)); + + const project = await Project.findOneAndUpdate( + { _id: projectId, owner: req.user._id }, + { $set: { externalConfig: encryptedConfig, isExternal: true } }, + { new: true } + ); + + if (!project) return res.status(404).json({ error: "Project not found." }); + + res.status(200).json({ message: "Configuration saved successfully." }); + } catch (error) { + res.status(500).json({ error: error.message }); + } } + module.exports.createCollection = async (req, res) => { try { const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body); @@ -122,13 +177,28 @@ module.exports.getData = 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." }); - const finalCollectionName = `${project._id}_${collectionName}`; - const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); + // const finalCollectionName = `${project._id}_${collectionName}`; - let data = []; - if (collectionsList.length > 0) { - data = await mongoose.connection.db.collection(finalCollectionName).find({}).limit(50).toArray(); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ + error: "Collection not found", + collection: collectionName + }); } + + const connection = await getConnection(projectId); + const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); + + const data = await model.find({}).limit(50); + + // let data = []; + // if (collectionsList.length > 0) { + // data = await mongoose.connection.db.collection(finalCollectionName).find({}).limit(50).toArray(); + // } + res.json(data); } catch (err) { res.status(500).json({ error: err.message }); @@ -143,18 +213,31 @@ module.exports.insertData = async (req, res) => { const finalCollectionName = `${project._id}_${collectionName}`; const incomingData = req.body; - const docSize = Buffer.byteLength(JSON.stringify(incomingData)); - const limit = project.databaseLimit || 50 * 1024 * 1024; - if ((project.databaseUsed || 0) + docSize > limit) { - return res.status(403).json({ error: "Database limit exceeded. Delete some data." }); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ error: "Collection configuration not found." }); + } + + let docSize = 0; + if (!project.isExternal) { + docSize = Buffer.byteLength(JSON.stringify(incomingData)); + + const limit = project.databaseLimit || 20 * 1024 * 1024; + + if ((project.databaseUsed || 0) + docSize > limit) { + return res.status(403).json({ error: "Database limit exceeded. Delete some data." }); + } } - const result = await mongoose.connection.db - .collection(finalCollectionName) - .insertOne(incomingData); + const connection = await getConnection(projectId); + const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + const result = await model.create(incomingData); - project.databaseUsed = (project.databaseUsed || 0) + docSize; + if (!project.isExternal) { + project.databaseUsed = (project.databaseUsed || 0) + docSize; + } await project.save(); res.json(result); @@ -166,27 +249,40 @@ module.exports.insertData = async (req, res) => { module.exports.deleteRow = async (req, res) => { try { const { projectId, collectionName, id } = req.params; + const project = await Project.findOne({ _id: projectId, owner: req.user._id }); if (!project) return res.status(404).json({ error: "Project not found." }); - const finalCollectionName = `${project._id}_${collectionName}`; - const collection = mongoose.connection.db.collection(finalCollectionName); + const collectionConfig = project.collections.find(c => c.name === collectionName); + if (!collectionConfig) { + return res.status(404).json({ error: "Collection not found." }); + } + + const connection = await getConnection(projectId); + const Model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + const docToDelete = await Model.findById(id); + if (!docToDelete) { + return res.status(404).json({ error: "Document not found." }); + } - const docToDelete = await collection.findOne({ _id: new mongoose.Types.ObjectId(id) }); + const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); - if (docToDelete) { - const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); - await collection.deleteOne({ _id: new mongoose.Types.ObjectId(id) }); + + await Model.deleteOne({ _id: id }); + + if (!project.isExternal) { project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); await project.save(); } - res.json({ success: true }); + res.json({ success: true, message: "Document deleted successfully" }); + } catch (err) { - console.log(err) + console.error("Delete Error:", err); res.status(500).json({ error: err.message }); } -} +}; module.exports.listFiles = async (req, res) => { try { diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index b75688364..cedcb33b9 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -28,6 +28,7 @@ module.exports = function (req, res, next) { // Proceed to the next middleware or route handler next(); } catch (err) { + console.log("err---------------2") console.error(err); // Do not expose detailed error information in production diff --git a/backend/models/Project.js b/backend/models/Project.js index a56b9fd4f..2a8497f22 100644 --- a/backend/models/Project.js +++ b/backend/models/Project.js @@ -15,6 +15,13 @@ const collectionSchema = new mongoose.Schema({ model: [fieldSchema] }); +const externalConfigSchema = new mongoose.Schema({ + encrypted: { type: String, select: false }, + iv: { type: String, select: false }, + tag: { type: String, select: false } +}, { _id: false }); + + const projectSchema = new mongoose.Schema({ name: { type: String, required: true }, description: String, @@ -34,13 +41,19 @@ const projectSchema = new mongoose.Schema({ collections: [collectionSchema], // STORAGE LIMITS (Files) - storageUsed: { type: Number, default: 0 }, // in bytes - storageLimit: { type: Number, default: 100 * 1024 * 1024 }, // 100MB for Files + storageUsed: { type: Number, default: 0 }, + storageLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Files // DATABASE LIMITS (JSON Docs) - databaseUsed: { type: Number, default: 0 }, // in bytes - databaseLimit: { type: Number, default: 50 * 1024 * 1024 } // 50MB for Data (approx 50,000 large docs) + databaseUsed: { type: Number, default: 0 }, // + databaseLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Data + + externalConfig: { + type: externalConfigSchema, + default: null + }, + isExternal: { type: Boolean, default: false } }, { timestamps: true }); module.exports = mongoose.model('Project', projectSchema); \ No newline at end of file diff --git a/backend/routes/projects.js b/backend/routes/projects.js index 2a403502d..bde8d17fd 100644 --- a/backend/routes/projects.js +++ b/backend/routes/projects.js @@ -20,6 +20,7 @@ const { deleteAllFiles, deleteProject, updateProject, + updateExternalConfig, analytics } = require("../controllers/project.controller") @@ -65,6 +66,9 @@ router.delete('/:projectId', authMiddleware, verifyEmail, deleteProject); // UPDATE PROJECT router.patch('/:projectId', authMiddleware, updateProject); +// UPDATE EXTERNAL CONFIG +router.patch('/:projectId/byod-config', authMiddleware, updateExternalConfig); + // INSERT DATA (Dashboard) router.post('/:projectId/collections/:collectionName/data', authMiddleware, verifyEmail, insertData); diff --git a/backend/utils/GC.js b/backend/utils/GC.js new file mode 100644 index 000000000..1dfb9b044 --- /dev/null +++ b/backend/utils/GC.js @@ -0,0 +1,21 @@ +const registry = require("./registry"); +function garbageCollect() { + setInterval(() => { + console.log("20 minutes passed"); + const now = new Date(); + for (const [key, value] of registry) { + if (now - value.lastAccessed > 20 * 60 * 1000) { + + if (value.readyState === 1) { + value.close(); + registry.delete(key); + } + + } + } + }, 20 * 60 * 1000); +} + +garbageCollect(); + +module.exports = garbageCollect; diff --git a/backend/utils/connectionManager.js b/backend/utils/connectionManager.js new file mode 100644 index 000000000..123d577f4 --- /dev/null +++ b/backend/utils/connectionManager.js @@ -0,0 +1,57 @@ +const registry = require("./registry"); +const Project = require("../models/Project"); +const { decrypt } = require("./encryption"); +const mongoose = require("mongoose"); + + +async function getConnection(projectId) { + const key = projectId.toString(); + if (registry.has(key)) { + const cachedConn = registry.get(key); + if (cachedConn.readyState === 1) { + cachedConn.lastAccessed = new Date(); + return cachedConn; + } + } + + const projectDoc = await Project.findById(projectId) + .select("+externalConfig.iv +externalConfig.tag +externalConfig.encrypted"); + if (!projectDoc) throw new Error("Project not found"); + if (!projectDoc.isExternal) { + return mongoose.connection; + } + + let config; + try { + const decryptedConfig = decrypt(projectDoc.externalConfig); + config = JSON.parse(decryptedConfig); + } catch { + throw new Error("Invalid or corrupted external config"); + } + + const connection = mongoose.createConnection(config.dbUri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + + connection.on("connected", () => { + console.log(`DB connected for project ${projectId}`); + }); + + connection.on("error", (err) => { + console.error("Connection error:", err); + }); + + connection.on("close", () => { + registry.delete(key); + console.log(`DB connection closed for project ${key}`); + }); + + + connection.lastAccessed = new Date(); + registry.set(key, connection); + + return connection; +} + +module.exports = { getConnection }; diff --git a/backend/utils/encryption.js b/backend/utils/encryption.js new file mode 100644 index 000000000..df37002f3 --- /dev/null +++ b/backend/utils/encryption.js @@ -0,0 +1,47 @@ +const crypto = require("crypto"); + +const algorithm = "aes-256-gcm"; +const ivLength = 16; + +const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY; + +function encrypt(plainText) { + if (!ENCRYPTION_KEY) throw new Error("ENCRYPTION_KEY missing in .env"); + + const iv = crypto.randomBytes(ivLength); + + const cipher = crypto.createCipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + + let encrypted = cipher.update(plainText, "utf8", "hex"); + encrypted += cipher.final("hex"); + + const tag = cipher.getAuthTag(); + + return { + iv: iv.toString("hex"), + encrypted: encrypted, + tag: tag.toString("hex") + }; +} + +function decrypt(encryptedData) { + try { + const iv = Buffer.from(encryptedData.iv, "hex"); + const tag = Buffer.from(encryptedData.tag, "hex"); + const encryptedText = Buffer.from(encryptedData.encrypted, "hex"); + + const decipher = crypto.createDecipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + decipher.setAuthTag(tag); + + let decrypted = decipher.update(encryptedText, "hex", "utf8"); + decrypted += decipher.final("utf8"); + + return decrypted; + } catch (error) { + console.error("Decryption failed: Data tampered or wrong key."); + return null; + } +} + + +module.exports = { encrypt, decrypt }; \ No newline at end of file diff --git a/backend/utils/injectModel.js b/backend/utils/injectModel.js new file mode 100644 index 000000000..498eac087 --- /dev/null +++ b/backend/utils/injectModel.js @@ -0,0 +1,66 @@ +const modelRegistry = new WeakMap(); +const mongoose = require("mongoose"); + +const typeMapping = { + String: String, + Number: Number, + Boolean: Boolean, + Date: Date +}; + +function buildMongooseSchema(fieldsArray) { + const schemaDef = {}; + + fieldsArray.forEach(field => { + schemaDef[field.key] = { + type: typeMapping[field.type], + required: !!field.required + }; + }); + + return new mongoose.Schema(schemaDef, { timestamps: true }); +} + + +function getCompiledModel(connection, collectionData, projectId, isExternal) { + + let collectionName = ""; + + if (!isExternal) { + collectionName = `${projectId}_${collectionData.name}` + } else { + collectionName = collectionData.name; + + } + + + // 1. Get per-connection cache + if (!modelRegistry.has(connection)) { + modelRegistry.set(connection, new Map()); + } + + const connectionModels = modelRegistry.get(connection); + + // 2. If already compiled for THIS connection + if (connectionModels.has(collectionName)) { + return connectionModels.get(collectionName); + } + + // 3. If model already exists on connection (edge case) + if (connection.models[collectionName]) { + const existingModel = connection.models[collectionName]; + connectionModels.set(collectionName, existingModel); + return existingModel; + } + + // 4. Build schema + compile + const schema = buildMongooseSchema(collectionData.model); + const model = connection.model(collectionName, schema); + + // 5. Cache it + connectionModels.set(collectionName, model); + + return model; +} + +module.exports = { getCompiledModel }; \ No newline at end of file diff --git a/backend/utils/registry.js b/backend/utils/registry.js new file mode 100644 index 000000000..1a3722137 --- /dev/null +++ b/backend/utils/registry.js @@ -0,0 +1,3 @@ +const registry = new Map(); + +module.exports = registry; diff --git a/frontend/src/components/Layout/Header.jsx b/frontend/src/components/Layout/Header.jsx index 3d57389b1..098118298 100644 --- a/frontend/src/components/Layout/Header.jsx +++ b/frontend/src/components/Layout/Header.jsx @@ -1,7 +1,7 @@ import { useAuth } from '../../context/AuthContext'; import { Menu } from 'lucide-react'; // Import Menu Icon -function Header({ onToggleSidebar }) { // Prop receive kiya +function Header({ onToggleSidebar, showToggle = true }) { // Default showToggle to true const { user } = useAuth(); const initial = user?.email ? user.email[0].toUpperCase() : 'D'; @@ -12,19 +12,18 @@ function Header({ onToggleSidebar }) { // Prop receive kiya borderBottom: '1px solid var(--color-border)', display: 'flex', alignItems: 'center', - justifyContent: 'space-between', // Changed to space-between - padding: '0 1rem', // Reduced padding for mobile + justifyContent: 'space-between', + padding: '0 1rem', position: 'fixed', top: 0, right: 0, - // Mobile fix: left should be 0, margin-left will handle desktop left: 0, - zIndex: 40, - // Desktop: Push header to right of sidebar - // We use a media query logic via inline style or keep consistent - // Better approach: Let CSS handle margins via .main-content structure + zIndex: 1000, width: '100%', - paddingLeft: 'calc(var(--sidebar-width) + 1rem)' // Desktop padding + // Only add paddingLeft if we expect a sidebar (showToggle is a proxy for sidebar existence here) + // But wait, showToggle means "can we toggle it". Even if we can't toggle, if sidebar is gone, padding should be 0 (or standard). + // In Project Mode, showToggle is false. So paddingLeft should be 0 (or standard 1rem from padding above). + paddingLeft: showToggle ? 'calc(var(--sidebar-width) + 1rem)' : '1rem' }} className="responsive-header"> {/* CSS override for mobile padding in style tag below */} @@ -42,14 +41,16 @@ function Header({ onToggleSidebar }) { // Prop receive kiya } `} - {/* Mobile Menu Button */} - + {/* Mobile Menu Button - Only show if toggle is allowed */} + {showToggle && ( + + )} {/* Spacer to push User Profile to right */}
@@ -72,4 +73,4 @@ function Header({ onToggleSidebar }) { // Prop receive kiya ); } -export default Header; \ No newline at end of file +export default Header; diff --git a/frontend/src/components/Layout/MainLayout.jsx b/frontend/src/components/Layout/MainLayout.jsx index a33655756..01c10871b 100644 --- a/frontend/src/components/Layout/MainLayout.jsx +++ b/frontend/src/components/Layout/MainLayout.jsx @@ -1,38 +1,54 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { useLocation, matchPath } from 'react-router-dom'; import Sidebar from './Sidebar'; import Header from './Header'; +import ProjectNavbar from './ProjectNavbar'; import logoImage from '../../assets/logo_u.png'; function MainLayout({ children }) { const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const location = useLocation(); + + // Check if we are inside a project route to toggle layout mode + // Paths like /project/:projectId/... + const isProjectRoute = matchPath("/project/:projectId/*", location.pathname); return (
{/* Mobile Overlay - Only visible when sidebar is open on mobile */} - {isSidebarOpen && ( + {isSidebarOpen && !isProjectRoute && (
setIsSidebarOpen(false)} >
)} - {/* Sidebar - Pass state and close function */} - setIsSidebarOpen(false)} - /> + {/* Sidebar - Only show if NOT in a project route (or if we want global sidebar always, but plan said hide it) */} + {!isProjectRoute && ( + setIsSidebarOpen(false)} + /> + )} {/* Main Content Area */} -
- {/* Header - Pass toggle function */} + {/* If Project Route, remove margin-left (full width) */} +
+ + {/* Global Header */}
setIsSidebarOpen(!isSidebarOpen)} + // Hide toggle button if sidebar is hidden + showToggle={!isProjectRoute} /> + {/* Project Navigation Bar - Only visible in project routes */} + {isProjectRoute && } + {/* Dynamic Page Content */} -
+
{children}
diff --git a/frontend/src/components/Layout/ProjectNavbar.jsx b/frontend/src/components/Layout/ProjectNavbar.jsx new file mode 100644 index 000000000..0d14be388 --- /dev/null +++ b/frontend/src/components/Layout/ProjectNavbar.jsx @@ -0,0 +1,57 @@ +import { NavLink, useParams, Link } from 'react-router-dom'; +import { + LayoutDashboard, Database, Shield, HardDrive, Settings, BarChart2, + ArrowLeft +} from 'lucide-react'; + +function ProjectNavbar() { + const { projectId } = useParams(); + + if (!projectId) return null; + + return ( +
+
+ + + Back + +
+
+ + +
+ ); +} + +export default ProjectNavbar; diff --git a/frontend/src/index.css b/frontend/src/index.css index 5d8e4b351..7257b9fd4 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,24 +1,39 @@ -/* --- GLOBAL DESIGN SYSTEM (Compact / Supabase-like) --- */ +/* --- GLOBAL DESIGN SYSTEM (Premium / Supabase-like) --- */ :root { - /* Colors */ + /* Colors - Core Black Theme (Supabase-like) */ --color-primary: #3ECF8E; --color-primary-hover: #34B27B; - --color-bg-main: #161616; - --color-bg-sidebar: #121212; - --color-bg-card: #1C1C1C; - --color-bg-input: #232323; + + --color-bg-main: #000000; + /* Pure Black */ + --color-bg-sidebar: #050505; + /* Very subtle differentiation */ + --color-bg-card: #0A0A0A; + /* Slightly lighter for cards */ + --color-bg-input: #0F0F0F; + /* Input fields */ + --color-text-main: #EDEDED; --color-text-muted: #888888; - --color-border: #2E2E2E; - --color-border-hover: #3E3E3E; - --color-danger: #EF4444; + + --color-border: #1E1E1E; + /* Very subtle borders */ + --color-border-hover: #333333; + + --color-danger: #ea5455; + /* Slightly more vibrant red */ --color-success: #3ECF8E; + --color-overlay: rgba(0, 0, 0, 0.8); - /* Dimensions (More Compact) */ - --sidebar-width: 240px; - --header-height: 50px; - /* Reduced from 64px */ - --border-radius: 4px; + /* Dimensions */ + --sidebar-width: 260px; + --header-height: 56px; + --border-radius: 6px; + + /* Glassmorphism - Darker & Smoother */ + --glass-bg: rgba(10, 10, 10, 0.7); + --glass-border: rgba(255, 255, 255, 0.05); + --glass-backdrop: blur(12px); } * { @@ -28,131 +43,217 @@ } body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background-color: var(--color-bg-main); color: var(--color-text-main); - line-height: 1.4; - font-size: 13px; - /* Smaller base font */ + line-height: 1.5; + font-size: 14px; + overflow-x: hidden; } /* --- UTILITY CLASSES --- */ .container { - max-width: 1200px; + max-width: 1400px; margin: 0 auto; - padding: 1.5rem; -} - -.page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 0.8rem; - border-bottom: 1px solid var(--color-border); + padding: 2rem; } -.page-title { - font-size: 1.2rem; - font-weight: 600; - color: var(--color-text-main); +/* Glassmorphism Panel */ +.glass-panel { + background: var(--glass-bg); + backdrop-filter: var(--glass-backdrop); + border: 1px solid var(--glass-border); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); } -/* Compact Cards */ +/* Premium Card */ .card { - background-color: var(--color-bg-card); + background: var(--color-bg-card); border: 1px solid var(--color-border); - border-radius: var(--border-radius); - padding: 1.2rem; - transition: border-color 0.2s; + border-radius: 12px; + padding: 1.5rem; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .card:hover { border-color: var(--color-border-hover); + box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.3); +} + +/* Scrollbar Styling */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg-main); } -/* Compact Buttons */ +::-webkit-scrollbar-thumb { + background: #333; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #444; +} + +/* Buttons */ .btn { display: inline-flex; align-items: center; - gap: 6px; - padding: 6px 12px; - /* Slimmer padding */ - border-radius: 4px; + justify-content: center; + gap: 8px; + padding: 8px 16px; + border-radius: var(--border-radius); font-weight: 500; - font-size: 0.85rem; + font-size: 0.9rem; cursor: pointer; border: 1px solid transparent; - transition: all 0.2s; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); text-decoration: none; - justify-content: center; + outline: none; +} + +.btn:active { + transform: scale(0.98); } .btn-primary { background-color: var(--color-primary); - color: #111; + color: #000; + border-color: var(--color-primary); + box-shadow: 0 2px 10px rgba(62, 207, 142, 0.2); } .btn-primary:hover { background-color: var(--color-primary-hover); + box-shadow: 0 4px 15px rgba(62, 207, 142, 0.3); } .btn-secondary { - background-color: transparent; + background-color: rgba(255, 255, 255, 0.03); border: 1px solid var(--color-border); color: var(--color-text-main); } .btn-secondary:hover { - background-color: rgba(255, 255, 255, 0.05); + background-color: rgba(255, 255, 255, 0.08); + border-color: var(--color-text-muted); } .btn-danger { - background-color: rgba(239, 68, 68, 0.1); + background-color: rgba(255, 95, 86, 0.1); color: var(--color-danger); - border: 1px solid rgba(239, 68, 68, 0.3); + border: 1px solid rgba(255, 95, 86, 0.2); } -.btn-ghost { - background: transparent; +.btn-danger:hover { + background-color: rgba(255, 95, 86, 0.15); + border-color: var(--color-danger); +} + +.btn-icon { + padding: 8px; + border-radius: var(--border-radius); color: var(--color-text-muted); + transition: all 0.2s; + background: transparent; + border: none; + cursor: pointer; } -.btn-ghost:hover { +.btn-icon:hover { color: var(--color-text-main); background: rgba(255, 255, 255, 0.05); } -/* Compact Inputs */ -.form-group { - margin-bottom: 1rem; -} - -.form-label { - display: block; - margin-bottom: 4px; - color: var(--color-text-muted); - font-size: 0.8rem; -} - +/* Form Elements */ .input-field { width: 100%; - padding: 8px 10px; - /* Slimmer input */ - border-radius: 4px; + padding: 10px 12px; + border-radius: var(--border-radius); border: 1px solid var(--color-border); background-color: var(--color-bg-input); color: var(--color-text-main); - font-size: 0.9rem; + font-size: 0.95rem; outline: none; - transition: border-color 0.2s; + transition: all 0.2s ease; } .input-field:focus { border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(62, 207, 142, 0.1); +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(5px); + } + + to { + opacity: 1; + transform: translateY(0); + } } -/* Layout & Sidebar */ +@keyframes slideIn { + from { + transform: translateX(-20px); + opacity: 0; + } + + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Skeleton Loading */ +.skeleton { + background: linear-gradient(90deg, #1c1c1c 25%, #2a2a2a 50%, #1c1c1c 75%); + background-size: 200% 100%; + animation: skeleton-loading 1.5s infinite; + border-radius: 4px; +} + +@keyframes skeleton-loading { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +/* Responsive Utilities */ +.hide-mobile { + display: block; +} + +.hide-desktop { + display: none; +} + +@media (max-width: 768px) { + .hide-mobile { + display: none !important; + } + + .hide-desktop { + display: block !important; + } + + :root { + --sidebar-width: 0px; + } +} + +/* --- LAYOUT STRUCTURE (Restored) --- */ .app-shell { display: flex; min-height: 100vh; @@ -167,6 +268,7 @@ body { display: flex; flex-direction: column; z-index: 50; + transition: transform 0.3s ease; } .sidebar-header { @@ -227,70 +329,37 @@ body { .main-content { margin-left: var(--sidebar-width); flex: 1; - padding-top: var(--header-height); + display: flex; + flex-direction: column; min-height: 100vh; - background-color: var(--color-bg-main); } .content-wrapper { + /* Push content down to account for fixed header */ + margin-top: var(--header-height); padding: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -/* Table Compact */ -table { - width: 100%; - border-collapse: collapse; - font-size: 0.85rem; -} - -th { - background-color: #1a1a1a; - color: var(--color-text-muted); - font-weight: 600; - text-align: left; - padding: 8px 12px; - border-bottom: 1px solid var(--color-border); -} - -td { - padding: 8px 12px; - border-bottom: 1px solid var(--color-border); - color: var(--color-text-main); -} - -tr:hover td { - background-color: rgba(255, 255, 255, 0.02); + flex: 1; } -/* --- RESPONSIVE DASHBOARD --- */ +/* Mobile Response for Layout */ @media (max-width: 768px) { .sidebar { transform: translateX(-100%); - /* Default hidden on mobile */ - transition: transform 0.3s ease-in-out; box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5); } .sidebar.mobile-open { transform: translateX(0); - /* Slide in */ } .main-content { margin-left: 0; - /* Remove margin on mobile */ - width: 100%; } - .page-header { - flex-direction: column; - align-items: flex-start; - gap: 1rem; + .content-wrapper { + padding: 1rem; } - /* Mobile Overlay (Black background when sidebar opens) */ .sidebar-overlay { position: fixed; top: 0; @@ -299,17 +368,153 @@ tr:hover td { bottom: 0; background: rgba(0, 0, 0, 0.7); z-index: 45; - /* Below sidebar (z-50) but above content */ animation: fadeIn 0.2s; } } -@keyframes fadeIn { - from { - opacity: 0; +/* --- PROJECT NAVIGATION BAR --- */ +.project-navbar { + background: var(--color-bg-card); + border-bottom: 1px solid var(--color-border); + padding: 0 2rem; + display: flex; + align-items: center; + gap: 2rem; + height: 56px; + position: sticky; + top: var(--header-height); + z-index: 40; + backdrop-filter: blur(10px); +} + +.nav-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.nav-back-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + color: var(--color-text-muted); + text-decoration: none; + border-radius: 6px; + transition: all 0.2s; + font-size: 0.9rem; +} + +.nav-back-btn:hover { + color: var(--color-text-main); + background: var(--color-bg-input); +} + +.nav-divider { + width: 1px; + height: 24px; + background: var(--color-border); +} + +.nav-links { + display: flex; + align-items: center; + gap: 0.25rem; + flex: 1; +} + +.nav-link { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + color: var(--color-text-muted); + text-decoration: none; + border-radius: 6px; + transition: all 0.2s; + font-size: 0.9rem; + font-weight: 500; + position: relative; +} + +.nav-link:hover { + color: var(--color-text-main); + background: rgba(255, 255, 255, 0.03); +} + +.nav-link.active { + color: var(--color-primary); + background: rgba(62, 207, 142, 0.08); +} + +.nav-link.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: var(--color-primary); + border-radius: 2px 2px 0 0; +} + +/* Responsive Navigation */ +@media (max-width: 768px) { + .project-navbar { + padding: 0 1rem; + gap: 1rem; + overflow-x: auto; } - to { - opacity: 1; + .nav-link span { + display: none; + } + + .nav-back-btn span { + display: none; + } + + .nav-divider { + display: none; } +} + +/* --- GLOBAL LAYOUT STRUCTURE --- */ +.app-shell { + display: flex; + flex-direction: row; + min-height: 100vh; + background: var(--color-bg-main); +} + +.sidebar { + width: var(--sidebar-width); + background: var(--color-bg-sidebar); + border-right: 1px solid var(--color-border); + position: fixed; + top: 0; + left: 0; + bottom: 0; + z-index: 50; + overflow-y: auto; +} + +.main-content { + flex: 1; + margin-left: var(--sidebar-width); + min-height: 100vh; + display: flex; + flex-direction: column; + transition: margin-left 0.3s ease; +} + +/* Full-width variant for project routes (no sidebar) */ +.main-content.full-width { + margin-left: 0; +} + +.content-wrapper { + flex: 1; + padding: 2rem; + overflow-y: auto; } \ No newline at end of file diff --git a/frontend/src/pages/Analytics.jsx b/frontend/src/pages/Analytics.jsx index a495be2b2..d9d9ad165 100644 --- a/frontend/src/pages/Analytics.jsx +++ b/frontend/src/pages/Analytics.jsx @@ -49,12 +49,12 @@ export default function Analytics() { }; return ( -
+
{/* Header with Refresh */} -
+
-

Project Analytics

-

Real-time metrics for your backend.

+

Analytics

+

Real-time usage metrics and logs.

+ + + ))} + + +
)}
+ +
); } \ No newline at end of file diff --git a/frontend/src/pages/CreateProject.jsx b/frontend/src/pages/CreateProject.jsx index 0ea4d8130..9a5a04ff7 100644 --- a/frontend/src/pages/CreateProject.jsx +++ b/frontend/src/pages/CreateProject.jsx @@ -3,7 +3,7 @@ import axios from 'axios'; import { useAuth } from '../context/AuthContext'; import { useNavigate } from 'react-router-dom'; import toast from 'react-hot-toast'; -import { ArrowLeft, Copy, CheckCircle, AlertTriangle } from 'lucide-react'; +import { ArrowLeft, Copy, CheckCircle, AlertTriangle, Plus } from 'lucide-react'; import { API_URL } from '../config'; @@ -71,39 +71,57 @@ function CreateProject() { // --- SUCCESS VIEW (API KEY) --- if (newProject) { return ( -
-
-
- -

Project Created!

-

{newProject.name} is ready to use.

+
+
+
+
+ +
+

Project Created!

+

{newProject.name} has been successfully initialized.

-
- +
+
- Save this API Key immediately -

- For security, we only show this key once. If you lose it, you will need to regenerate it, which may break your app. + Save this API Key immediately +

+ For security reasons, this key will only be shown once. If you lose it, you will need to regenerate it, which may interrupt your application.

-
- +
+
- {newProject.apiKey} - +
@@ -113,7 +131,7 @@ function CreateProject() { @@ -124,48 +142,61 @@ function CreateProject() { // --- FORM VIEW --- return ( -
+
-
-

Create New Project

+
+
+

+
+ +
+ Create New Project +

+

+ Initialize a new backend project with database and storage. +

+
+
-
- +
+ setName(e.target.value)} className="input-field" - placeholder="e.g. My E-commerce API" + placeholder="e.g. E-commerce API" autoFocus + style={{ padding: '12px', background: 'var(--color-bg-input)', border: '1px solid var(--color-border)', color: '#fff' }} />
-
- +
+