From 29927e581a3168eae9d8dd52a8ed79282cafb79f Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Sat, 3 Jan 2026 01:27:33 +0530 Subject: [PATCH 1/4] feat: setup BYOD foundation (encryption & connection manager) - Add encryption utilities. - Create basic connection registry. - Add API to update external config. - Note: GC and Middleware pending. --- backend/.gitignore | 3 +- backend/controllers/project.controller.js | 38 +++++++++++++++ backend/models/Project.js | 21 +++++++-- backend/routes/projects.js | 4 ++ backend/utils/connectionManager.js | 51 ++++++++++++++++++++ backend/utils/encryption.js | 47 +++++++++++++++++++ backend/utils/injectModel.js | 57 +++++++++++++++++++++++ 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 backend/utils/connectionManager.js create mode 100644 backend/utils/encryption.js create mode 100644 backend/utils/injectModel.js 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/controllers/project.controller.js b/backend/controllers/project.controller.js index 714d54b9d..5256509e3 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -3,6 +3,7 @@ const Project = require("../models/Project") const { createProjectSchema, createCollectionSchema } = require('../utils/input.validation'); const { generateApiKey, hashApiKey } = require('../utils/api'); const { z } = require('zod'); +const { encrypt, decrypt } = require('../utils/encryption'); @@ -89,6 +90,43 @@ module.exports.regenerateApiKey = async (req, res) => { } } +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: "All external configuration fields are required." }); + } + + + const configData = JSON.stringify({ + dbUri, + storageUrl, + storageKey, + storageProvider, + }); + + const encryptedConfig = encrypt(configData); + + + + 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: "External config updated 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); 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/connectionManager.js b/backend/utils/connectionManager.js new file mode 100644 index 000000000..1895203ee --- /dev/null +++ b/backend/utils/connectionManager.js @@ -0,0 +1,51 @@ +const registry = new Map(); +const Project = require("../models/Project"); +const { decrypt } = require("./encryption"); +const mongoose = require("mongoose"); + + +async function getConnection(projectId) { + if (registry.has(projectId)) { + const cachedConn = registry.get(projectId); + if (cachedConn.readyState === 1) { + 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) throw new Error("Project is not external"); + + 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(projectId); + console.log(`DB connection closed for project ${projectId}`); + }); + + + registry.set(projectId, 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..17afe4c5b --- /dev/null +++ b/backend/utils/injectModel.js @@ -0,0 +1,57 @@ +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) { + const 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 From e90b0f0cb561b3a2bef31261db3e361d8c8f737c Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Sat, 3 Jan 2026 20:27:46 +0530 Subject: [PATCH 2/4] feat(byod): implement connection pooling, GC, and frontend settings Backend: - Added registry.js for active database connection caching. - Implemented GC.js to auto-close idle connections (>20m). - Updated connectionManager to use registry for connection pooling. - Initialized Garbage Collection in app.js. Frontend: - Added ExternalConfigForm in ProjectSettings for BYOD setup. - Users can now configure/update their own MongoDB and Storage details. --- backend/app.js | 1 + backend/utils/GC.js | 21 ++++ backend/utils/connectionManager.js | 16 ++- backend/utils/registry.js | 3 + frontend/src/pages/ProjectSettings.jsx | 165 +++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 backend/utils/GC.js create mode 100644 backend/utils/registry.js diff --git a/backend/app.js b/backend/app.js index 1cef5c7af..c454cf893 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 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 index 1895203ee..01245766d 100644 --- a/backend/utils/connectionManager.js +++ b/backend/utils/connectionManager.js @@ -1,13 +1,15 @@ -const registry = new Map(); +const registry = require("./registry"); const Project = require("../models/Project"); const { decrypt } = require("./encryption"); const mongoose = require("mongoose"); async function getConnection(projectId) { - if (registry.has(projectId)) { - const cachedConn = registry.get(projectId); + const key = projectId.toString(); + if (registry.has(key)) { + const cachedConn = registry.get(key); if (cachedConn.readyState === 1) { + cachedConn.lastAccessed = new Date(); return cachedConn; } } @@ -39,12 +41,14 @@ async function getConnection(projectId) { }); connection.on("close", () => { - registry.delete(projectId); - console.log(`DB connection closed for project ${projectId}`); + registry.delete(key); + console.log(`DB connection closed for project ${key}`); }); - registry.set(projectId, connection); + connection.lastAccessed = new Date(); + registry.set(key, connection); + return connection; } 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/pages/ProjectSettings.jsx b/frontend/src/pages/ProjectSettings.jsx index 58790e2ba..2669ff85b 100644 --- a/frontend/src/pages/ProjectSettings.jsx +++ b/frontend/src/pages/ProjectSettings.jsx @@ -108,6 +108,9 @@ export default function ProjectSettings() { + {/* External Configuration */} + + {/* Danger Zone */}
@@ -145,4 +148,166 @@ export default function ProjectSettings() {
); +} + +function ExternalConfigForm({ project, projectId, token }) { + const [config, setConfig] = useState({ + dbUri: '', + storageUrl: '', + storageKey: '', + storageProvider: 'supabase' // default + }); + const [loading, setLoading] = useState(false); + const [isConfigured, setIsConfigured] = useState(project?.isExternal || false); + const [showForm, setShowForm] = useState(!project?.isExternal); + + useEffect(() => { + setIsConfigured(project?.isExternal || false); + // If it's not configured, always show form. If it is, hide it by default. + setShowForm(!project?.isExternal); + }, [project]); + + const handleChange = (e) => { + setConfig(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleUpdateConfig = async () => { + if (!config.dbUri || !config.storageUrl || !config.storageKey) { + return toast.error("All external configuration fields are required."); + } + + setLoading(true); + try { + await axios.patch(`${API_URL}/api/projects/${projectId}/byod-config`, + config, + { headers: { Authorization: `Bearer ${token}` } } + ); + toast.success("External configuration updated successfully!"); + + // Update local state to reflect success + setIsConfigured(true); + setShowForm(false); + + setConfig({ + dbUri: '', + storageUrl: '', + storageKey: '', + storageProvider: 'supabase' + }); + } catch (err) { + toast.error(err.response?.data?.error || "Failed to update configuration"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

External Configuration

+

+ Connect your own database and storage.
+

+ + {isConfigured && !showForm ? ( +
+
+
+ Connected Successfully +
+

+ Your project is currently using external database and storage. +

+ +
+ ) : ( + <> +

+ {isConfigured + ? "Note: Updating this will overwrite your existing configuration." + : "Configure your external resources to scale beyond the free tier."} +

+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ {isConfigured && ( + + )} + +
+
+ + )} +
+
+ ); } \ No newline at end of file From 2943c0d7461ac3d2e5ba5d9dd5ee3190f42b6d6b Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Sun, 4 Jan 2026 23:28:47 +0530 Subject: [PATCH 3/4] feat(byod): implement core dynamic DB connection and model injection logic --- backend/controllers/project.controller.js | 32 ++++++++++++++++++----- backend/utils/connectionManager.js | 4 ++- backend/utils/injectModel.js | 13 +++++++-- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 5256509e3..76663173c 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -3,8 +3,9 @@ const Project = require("../models/Project") const { createProjectSchema, createCollectionSchema } = require('../utils/input.validation'); const { generateApiKey, hashApiKey } = require('../utils/api'); const { z } = require('zod'); -const { encrypt, decrypt } = require('../utils/encryption'); - +const { encrypt } = require('../utils/encryption'); +const { getConnection } = require("../utils/connectionManager"); +const { getCompiledModel } = require("../utils/injectModel") // CONFIGURATION @@ -161,12 +162,27 @@ module.exports.getData = async (req, res) => { 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(); - 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 }); @@ -182,12 +198,14 @@ 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; + 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); diff --git a/backend/utils/connectionManager.js b/backend/utils/connectionManager.js index 01245766d..123d577f4 100644 --- a/backend/utils/connectionManager.js +++ b/backend/utils/connectionManager.js @@ -17,7 +17,9 @@ async function getConnection(projectId) { const projectDoc = await Project.findById(projectId) .select("+externalConfig.iv +externalConfig.tag +externalConfig.encrypted"); if (!projectDoc) throw new Error("Project not found"); - if (!projectDoc.isExternal) throw new Error("Project is not external"); + if (!projectDoc.isExternal) { + return mongoose.connection; + } let config; try { diff --git a/backend/utils/injectModel.js b/backend/utils/injectModel.js index 17afe4c5b..498eac087 100644 --- a/backend/utils/injectModel.js +++ b/backend/utils/injectModel.js @@ -22,8 +22,17 @@ function buildMongooseSchema(fieldsArray) { } -function getCompiledModel(connection, collectionData) { - const collectionName = collectionData.name; +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)) { From 9df1d4217c13b098c03a16dcf2d84854be605461 Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Mon, 5 Jan 2026 16:48:51 +0530 Subject: [PATCH 4/4] feat: BYOD support with Core Black UI refresh Implemented Bring Your Own Database (BYOD) with dynamic MongoDB connections Added external DB URI configuration and connection pooling with GC Introduced backend utilities for connection management and lifecycle control Refreshed UI with Supabase-inspired Core Black theme across all pages Added ProjectNavbar and BYOD settings UI Fixed critical layout and crash issues; standardized global card styles --- backend/app.js | 12 +- backend/controllers/data.controller.js | 226 ++---- backend/controllers/project.controller.js | 106 ++- backend/middleware/authMiddleware.js | 1 + frontend/src/components/Layout/Header.jsx | 37 +- frontend/src/components/Layout/MainLayout.jsx | 38 +- .../src/components/Layout/ProjectNavbar.jsx | 57 ++ frontend/src/index.css | 443 ++++++++--- frontend/src/pages/Analytics.jsx | 127 ++- frontend/src/pages/Auth.jsx | 126 +-- frontend/src/pages/CreateProject.jsx | 95 ++- frontend/src/pages/Dashboard.jsx | 119 +-- frontend/src/pages/Database.jsx | 734 +++++++++++++----- frontend/src/pages/Login.jsx | 104 ++- frontend/src/pages/ProjectDetails.jsx | 81 +- frontend/src/pages/ProjectSettings.jsx | 121 +-- frontend/src/pages/Settings.jsx | 109 +-- frontend/src/pages/Signup.jsx | 144 ++-- frontend/src/pages/Storage.jsx | 92 ++- 19 files changed, 1770 insertions(+), 1002 deletions(-) create mode 100644 frontend/src/components/Layout/ProjectNavbar.jsx diff --git a/backend/app.js b/backend/app.js index c454cf893..308da749f 100644 --- a/backend/app.js +++ b/backend/app.js @@ -54,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 76663173c..15ccf6a16 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,9 +1,11 @@ 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") @@ -89,39 +91,53 @@ 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: "All external configuration fields are required." }); + 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 configData = JSON.stringify({ - dbUri, - storageUrl, - storageKey, - storageProvider, - }); - - const encryptedConfig = encrypt(configData); - + 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." }); + if (!project) return res.status(404).json({ error: "Project not found." }); - res.status(200).json({ message: "External config updated successfully." }); + res.status(200).json({ message: "Configuration saved successfully." }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -161,7 +177,7 @@ 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 finalCollectionName = `${project._id}_${collectionName}`; const collectionConfig = project.collections.find(c => c.name === collectionName); if (!collectionConfig) { @@ -197,20 +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 || 20 * 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; - const result = await mongoose.connection.db - .collection(finalCollectionName) - .insertOne(incomingData); + if ((project.databaseUsed || 0) + docSize > limit) { + return res.status(403).json({ error: "Database limit exceeded. Delete some data." }); + } + } - project.databaseUsed = (project.databaseUsed || 0) + docSize; + const connection = await getConnection(projectId); + const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + + const result = await model.create(incomingData); + + if (!project.isExternal) { + project.databaseUsed = (project.databaseUsed || 0) + docSize; + } await project.save(); res.json(result); @@ -222,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 docToDelete = await collection.findOne({ _id: new mongoose.Types.ObjectId(id) }); + const connection = await getConnection(projectId); + const Model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); - if (docToDelete) { - const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); - await collection.deleteOne({ _id: new mongoose.Types.ObjectId(id) }); + const docToDelete = await Model.findById(id); + if (!docToDelete) { + return res.status(404).json({ error: "Document not found." }); + } + + const docSize = Buffer.byteLength(JSON.stringify(docToDelete)); + + + 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/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' }} />
-
- +
+