diff --git a/backend/controllers/data.controller.js b/backend/controllers/data.controller.js index 5800f0fa6..063c29cac 100644 --- a/backend/controllers/data.controller.js +++ b/backend/controllers/data.controller.js @@ -1,7 +1,7 @@ const { sanitize } = require("../utils/input.validation"); const mongoose = require('mongoose'); const Project = require("../models/Project"); -const { getConnection } = require("../utils/connectionManager"); +const { getConnection } = require("../utils/connection.manager"); const { getCompiledModel } = require("../utils/injectModel"); // Helper: CodeQL ko satisfy karne ke liye ID validate karna zaroori hai @@ -42,7 +42,7 @@ module.exports.insertData = async (req, res) => { const safeData = sanitize(cleanData); let docSize = 0; - if (!project.isExternal) { + if (!project.resources.db.isExternal) { docSize = Buffer.byteLength(JSON.stringify(safeData)); if ((project.databaseUsed || 0) + docSize > project.databaseLimit) { return res.status(403).json({ error: "Database limit exceeded." }); @@ -50,11 +50,11 @@ module.exports.insertData = async (req, res) => { } const connection = await getConnection(project._id); - const Model = getCompiledModel(connection, collectionConfig, project._id, project.isExternal); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); const result = await Model.create(safeData); - if (!project.isExternal) { + if (!project.resources.db.isExternal) { project.databaseUsed = (project.databaseUsed || 0) + docSize; await project.save(); } @@ -75,7 +75,7 @@ module.exports.getAllData = async (req, res) => { 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); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); const data = await Model.find({}).limit(100).lean(); res.json(data); @@ -97,7 +97,7 @@ module.exports.getSingleDoc = async (req, res) => { 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); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); const doc = await Model.findById(id).lean(); if (!doc) return res.status(404).json({ error: "Document not found." }); @@ -121,7 +121,7 @@ module.exports.updateSingleData = async (req, res) => { 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); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); // Strict Schema Validation const schemaRules = collectionConfig.model; @@ -161,19 +161,19 @@ module.exports.deleteSingleDoc = async (req, res) => { 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); + const Model = getCompiledModel(connection, collectionConfig, project._id, project.resources.db.isExternal); const docToDelete = await Model.findById(id); if (!docToDelete) return res.status(404).json({ error: "Document not found." }); let docSize = 0; - if (!project.isExternal) { + if (!project.resources.db.isExternal) { docSize = Buffer.byteLength(JSON.stringify(docToDelete)); } await Model.deleteOne({ _id: id }); - if (!project.isExternal) { + if (!project.resources.db.isExternal) { project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); await project.save(); } diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 15ccf6a16..49ea6b4cd 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,21 +1,25 @@ const mongoose = require("mongoose") const Project = require("../models/Project") const Log = require("../models/Log") -const { createProjectSchema, createCollectionSchema } = require('../utils/input.validation'); +const { getStorage } = require("../utils/storage.manager"); +const { randomUUID } = require("crypto"); +const { createProjectSchema, createCollectionSchema, updateExternalConfigSchema } = 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 { getConnection } = require("../utils/connection.manager"); const { getCompiledModel } = require("../utils/injectModel") +const { storageRegistry } = require("../utils/registry"); -// CONFIGURATION -const multer = require('multer'); -const { createClient } = require('@supabase/supabase-js'); -const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY); -exports.supabase = supabase; -const storage = multer.memoryStorage(); + +const getBucket = (project) => + project.resources?.storage?.isExternal ? "files" : "dev-files"; + +const isExternalStorage = (project) => + !!project.resources?.storage?.isExternal; + module.exports.createProject = async (req, res) => { @@ -109,37 +113,52 @@ const isSafeUri = (uri) => { 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." - }); - } + // 1. Zod Validation + const validatedData = updateExternalConfigSchema.parse(req.body); + const { dbUri, storageUrl, storageKey, storageProvider } = validatedData; - if (dbUri && !isSafeUri(dbUri)) return res.status(400).json({ error: "Invalid DB URI" }); + const updateData = {}; - const configToEncrypt = {}; - if (dbUri) configToEncrypt.dbUri = dbUri; - if (storageUrl && storageKey) { - configToEncrypt.storageUrl = storageUrl; - configToEncrypt.storageKey = storageKey; - configToEncrypt.storageProvider = storageProvider || 'supabase'; + // 2. Database URI Check & Encryption + if (dbUri) { + if (!isSafeUri(dbUri)) return res.status(400).json({ error: "DB URI is pointing to a restricted host (localhost/internal)." }); + + // Naye model structure ke hisaab se save karein + updateData['resources.db.config'] = encrypt(JSON.stringify({ dbUri })); + updateData['resources.db.isExternal'] = true; } - const encryptedConfig = encrypt(JSON.stringify(configToEncrypt)); + // 3. Storage Config Encryption + if (storageUrl && storageKey) { + const storageConfig = { + storageUrl, + storageKey, + storageProvider: storageProvider || 'supabase' + }; + updateData['resources.storage.config'] = encrypt(JSON.stringify(storageConfig)); + updateData['resources.storage.isExternal'] = true; + } const project = await Project.findOneAndUpdate( { _id: projectId, owner: req.user._id }, - { $set: { externalConfig: encryptedConfig, isExternal: true } }, + { $set: updateData }, { new: true } ); - if (!project) return res.status(404).json({ error: "Project not found." }); + if (!project) return res.status(404).json({ error: "Project not found or access denied." }); + + res.status(200).json({ message: "External configuration updated successfully." }); + } catch (err) { + // Zod Error handling ko safe banayein + if (err.name === 'ZodError') { + return res.status(400).json({ + error: err.errors?.[0]?.message || err.issues?.[0]?.message || "Validation failed" + }); + } - res.status(200).json({ message: "Configuration saved successfully." }); - } catch (error) { - res.status(500).json({ error: error.message }); + console.error("External Config Error:", err); + res.status(500).json({ error: err.message }); } } @@ -188,7 +207,7 @@ module.exports.getData = async (req, res) => { } const connection = await getConnection(projectId); - const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + const model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal); // const collectionsList = await mongoose.connection.db.listCollections({ name: finalCollectionName }).toArray(); @@ -220,7 +239,7 @@ module.exports.insertData = async (req, res) => { } let docSize = 0; - if (!project.isExternal) { + if (!project.resources.db.isExternal) { docSize = Buffer.byteLength(JSON.stringify(incomingData)); const limit = project.databaseLimit || 20 * 1024 * 1024; @@ -231,11 +250,11 @@ module.exports.insertData = async (req, res) => { } const connection = await getConnection(projectId); - const model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + const model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal); const result = await model.create(incomingData); - if (!project.isExternal) { + if (!project.resources.db.isExternal) { project.databaseUsed = (project.databaseUsed || 0) + docSize; } await project.save(); @@ -259,7 +278,7 @@ module.exports.deleteRow = async (req, res) => { } const connection = await getConnection(projectId); - const Model = getCompiledModel(connection, collectionConfig, projectId, project.isExternal); + const Model = getCompiledModel(connection, collectionConfig, projectId, project.resources.db.isExternal); const docToDelete = await Model.findById(id); if (!docToDelete) { @@ -271,7 +290,7 @@ module.exports.deleteRow = async (req, res) => { await Model.deleteOne({ _id: id }); - if (!project.isExternal) { + if (!project.resources.db.isExternal) { project.databaseUsed = Math.max(0, (project.databaseUsed || 0) - docSize); await project.save(); } @@ -287,109 +306,178 @@ module.exports.deleteRow = async (req, res) => { module.exports.listFiles = async (req, res) => { try { const { projectId } = req.params; - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + + const project = await Project.findOne({ _id: projectId, owner: req.user._id }) + .select("+resources.storage.config.encrypted +resources.storage.config.iv +resources.storage.config.tag resources.storage.isExternal storageUsed storageLimit"); if (!project) return res.status(404).json({ error: "Project not found" }); - const { data, error } = await supabase.storage.from('dev-files').list(`${projectId}`, { - limit: 100, - sortBy: { column: 'created_at', order: 'desc' }, - }); + const supabase = await getStorage(project); + const bucket = getBucket(project); + + const { data, error } = await supabase.storage + .from(bucket) + .list(`${projectId}`, { + limit: 100, + sortBy: { column: "created_at", order: "desc" } + }); if (error) throw error; const files = data.map(file => { - const { data: publicUrlData } = supabase.storage.from("dev-files").getPublicUrl(`${projectId}/${file.name}`); - return { ...file, publicUrl: publicUrlData.publicUrl, path: `${projectId}/${file.name}` }; + const { data: url } = supabase.storage + .from(bucket) + .getPublicUrl(`${projectId}/${file.name}`); + + return { + ...file, + path: `${projectId}/${file.name}`, + publicUrl: url.publicUrl + }; }); res.json(files); } catch (err) { - console.log(err) res.status(500).json({ error: err.message }); } -} +}; + module.exports.uploadFile = async (req, res) => { try { const { projectId } = req.params; const file = req.file; + if (!file) return res.status(400).json({ error: "No file uploaded" }); - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + const project = await Project.findOne({ _id: projectId, owner: req.user._id }) + .select("+resources.storage.config.encrypted +resources.storage.config.iv +resources.storage.config.tag resources.storage.isExternal storageUsed storageLimit"); if (!project) return res.status(404).json({ error: "Project not found" }); - if (project.storageUsed + file.size > project.storageLimit) { - return res.status(403).json({ error: "Storage limit exceeded. Delete some files." }); + const external = isExternalStorage(project); + + if (!external) { + if (project.storageUsed + file.size > project.storageLimit) { + return res.status(403).json({ error: "Storage limit exceeded" }); + } } - const fileName = `${projectId}/${Date.now()}_${file.originalname.replace(/\s+/g, '_')}`; + const supabase = await getStorage(project); + const bucket = getBucket(project); - const { error } = await supabase.storage.from("dev-files").upload(fileName, file.buffer, { - contentType: file.mimetype - }); + const safeName = file.originalname.replace(/\s+/g, "_"); + const path = `${projectId}/${randomUUID()}_${safeName}`; + + const { error } = await supabase.storage + .from(bucket) + .upload(path, file.buffer, { + contentType: file.mimetype, + upsert: false + }); if (error) throw error; - project.storageUsed += file.size; - await project.save(); + if (!external) { + project.storageUsed += file.size; + await project.save(); + } - res.json({ message: "Uploaded" }); + res.json({ success: true, path }); } catch (err) { - res.status(500).json({ error: err.message }); + console.log("upload file ke catch me hu"); + res.status(500).json({ error: err }); } -} +}; + module.exports.deleteFile = async (req, res) => { try { const { projectId } = req.params; const { path } = req.body; - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + if (!path) return res.status(400).json({ error: "Path required" }); + + const project = await Project.findOne({ _id: projectId, owner: req.user._id }) + .select("+resources.storage.config.encrypted +resources.storage.config.iv +resources.storage.config.tag resources.storage.isExternal storageUsed storageLimit"); if (!project) return res.status(404).json({ error: "Project not found" }); - const { error } = await supabase.storage.from('dev-files').remove([path]); + if (!path.startsWith(`${projectId}/`)) { + return res.status(403).json({ error: "Access denied" }); + } + + const supabase = await getStorage(project); + const bucket = getBucket(project); + const external = isExternalStorage(project); + + let fileSize = 0; + + if (!external) { + const { data } = await supabase.storage + .from(bucket) + .list(projectId, { + search: path.split("/").pop() + }); + + if (data?.length) { + fileSize = data[0]?.metadata?.size || 0; + } + } + + const { error } = await supabase.storage.from(bucket).remove([path]); if (error) throw error; + if (!external && fileSize > 0) { + project.storageUsed = Math.max(0, project.storageUsed - fileSize); + await project.save(); + } + res.json({ success: true }); } catch (err) { res.status(500).json({ error: err.message }); } -} +}; + module.exports.deleteAllFiles = async (req, res) => { try { const { projectId } = req.params; - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + + const project = await Project.findOne({ _id: projectId, owner: req.user._id }) + .select("+resources.storage.config.encrypted +resources.storage.config.iv +resources.storage.config.tag resources.storage.isExternal storageUsed storageLimit"); if (!project) return res.status(404).json({ error: "Project not found" }); - let deletedCount = 0; + const supabase = await getStorage(project); + const bucket = getBucket(project); + let hasMore = true; + let deleted = 0; while (hasMore) { - const { data: files, error } = await supabase.storage - .from('dev-files') + const { data, error } = await supabase.storage + .from(bucket) .list(projectId, { limit: 100 }); if (error) throw error; - if (files && files.length > 0) { - const paths = files.map(f => `${projectId}/${f.name}`); - const { error: delError } = await supabase.storage - .from('dev-files') - .remove(paths); - - if (delError) throw delError; - deletedCount += files.length; - } else { + if (data.length === 0) { hasMore = false; + } else { + const paths = data.map(f => `${projectId}/${f.name}`); + await supabase.storage.from(bucket).remove(paths); + deleted += data.length; } } - res.json({ success: true, count: deletedCount }); + if (!isExternalStorage(project)) { + project.storageUsed = 0; + await project.save(); + } + + res.json({ success: true, deleted }); } catch (err) { res.status(500).json({ error: err.message }); } -} +}; + module.exports.updateProject = async (req, res) => { try { @@ -409,7 +497,8 @@ module.exports.updateProject = async (req, res) => { module.exports.deleteProject = async (req, res) => { try { const projectId = req.params.projectId; - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + const project = await Project.findOne({ _id: projectId, owner: req.user._id }) + .select("+resources.storage.config.encrypted +resources.storage.config.iv +resources.storage.config.tag resources.storage.isExternal storageUsed storageLimit"); if (!project) return res.status(404).json({ error: "Project not found or access denied." }); for (const col of project.collections) { @@ -423,27 +512,31 @@ module.exports.deleteProject = async (req, res) => { await mongoose.connection.db.dropCollection(`${project._id}_users`); } catch (e) { } + // DELETE ALL FILES (BYOS SAFE) + const supabase = await getStorage(project); + const bucket = getBucket(project); + let hasMoreFiles = true; + while (hasMoreFiles) { const { data: files, error } = await supabase.storage - .from('dev-files') - .list(`${projectId}`, { limit: 100 }); + .from(bucket) + .list(projectId, { limit: 100 }); if (error) throw error; if (files && files.length > 0) { - const pathsToRemove = files.map(f => `${projectId}/${f.name}`); - const { error: removeError } = await supabase.storage - .from('dev-files') - .remove(pathsToRemove); - - if (removeError) throw removeError; + const paths = files.map(f => `${projectId}/${f.name}`); + await supabase.storage.from(bucket).remove(paths); } else { hasMoreFiles = false; } } + await Project.deleteOne({ _id: projectId }); + storageRegistry.delete(projectId.toString()); + res.json({ message: "Project and all associated resources deleted successfully" }); } catch (err) { console.error(err); diff --git a/backend/controllers/storage.controller.js b/backend/controllers/storage.controller.js index 86236190d..c68d8ef98 100644 --- a/backend/controllers/storage.controller.js +++ b/backend/controllers/storage.controller.js @@ -1,99 +1,196 @@ -const { createClient } = require('@supabase/supabase-js'); +const { getStorage } = require("../utils/storage.manager"); +const { randomUUID } = require("crypto"); -const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY); +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +const getBucket = (project) => + project.resources?.storage?.isExternal ? "files" : "dev-files"; +const isExternal = (project) => + !!project.resources?.storage?.isExternal; +/** + * Upload File + */ module.exports.uploadFile = async (req, res) => { try { const file = req.file; - if (!file) return res.status(400).json({ error: "No file uploaded." }); + if (!file) { + return res.status(400).json({ error: "No file uploaded." }); + } - const project = req.project; + if (file.size > MAX_FILE_SIZE) { + return res.status(413).json({ error: "File size exceeds limit." }); + } - if (project.storageUsed + file.size > project.storageLimit) { - return res.status(403).json({ error: "Storage limit exceeded. Upgrade plan or delete files." }); + const project = req.project; + const external = isExternal(project); + const bucket = getBucket(project); + + if (!external) { + if (project.storageUsed + file.size > project.storageLimit) { + return res + .status(403) + .json({ error: "Internal storage limit exceeded." }); + } } - const fileName = `${project._id}/${Date.now()}_${file.originalname.replace(/\s+/g, '_')}`; + const supabase = await getStorage(project); - const { data, error } = await supabase.storage - .from("dev-files") - .upload(fileName, file.buffer, { contentType: file.mimetype }); + const safeName = file.originalname.replace(/\s+/g, "_"); + const filePath = `${project._id}/${randomUUID()}_${safeName}`; - if (error) throw error; + const { error: uploadError } = await supabase.storage + .from(bucket) + .upload(filePath, file.buffer, { + contentType: file.mimetype, + upsert: false + }); - project.storageUsed += file.size; - await project.save(); + if (uploadError) throw uploadError; + + if (!external) { + project.storageUsed += file.size; + await project.save(); + } const { data: publicUrlData } = supabase.storage - .from("dev-files") - .getPublicUrl(fileName); + .from(bucket) + .getPublicUrl(filePath); - res.status(201).json({ + return res.status(201).json({ message: "File uploaded successfully", url: publicUrlData.publicUrl, - path: fileName + path: filePath, + provider: external ? "external" : "internal" }); - } catch (err) { - res.status(500).json({ error: err.message }); + return res.status(500).json({ + error: "File upload failed", + details: + process.env.NODE_ENV === "development" + ? err.message + : undefined + }); } -} +}; +/** + * Delete File + */ module.exports.deleteFile = async (req, res) => { try { const { path } = req.body; - const project = req.project; + if (!path) { + return res.status(400).json({ error: "File path is required." }); + } - if (!path) return res.status(400).json({ error: "File path is required." }); + const project = req.project; + const external = isExternal(project); + const bucket = getBucket(project); if (!path.startsWith(`${project._id}/`)) { - return res.status(403).json({ error: "Access denied. You can only delete your own project files." }); + return res.status(403).json({ error: "Access denied." }); + } + + const supabase = await getStorage(project); + + // Fetch metadata before delete (for internal storage accounting) + let fileSize = 0; + if (!external) { + const { data, error } = await supabase.storage + .from(bucket) + .list(path.split("/")[0], { + search: path.split("/").slice(1).join("/") + }); + + if (error) throw error; + if (data?.length) { + fileSize = data[0].metadata?.size || 0; + } } - const { error } = await supabase.storage.from('dev-files').remove([path]); - if (error) throw error; + const { error: deleteError } = await supabase.storage + .from(bucket) + .remove([path]); - res.json({ message: "File deleted successfully" }); + if (deleteError) throw deleteError; + if (!external && fileSize > 0) { + project.storageUsed = Math.max( + 0, + project.storageUsed - fileSize + ); + await project.save(); + } + + return res.json({ message: "File deleted successfully" }); } catch (err) { - res.status(500).json({ error: err.message }); + return res.status(500).json({ + error: "File deletion failed", + details: + process.env.NODE_ENV === "development" + ? err.message + : undefined + }); } -} - +}; module.exports.deleteAllFiles = async (req, res) => { try { - const project = req.project; - const projectId = project._id.toString(); + const project = req.project; // assuming middleware attaches project + if (!project) { + return res.status(404).json({ error: "Project not found" }); + } + + const supabase = await getStorage(project); + const bucket = getBucket(project); - let deletedCount = 0; let hasMore = true; + let deletedCount = 0; while (hasMore) { const { data: files, error } = await supabase.storage - .from('dev-files') - .list(projectId, { limit: 100 }); + .from(bucket) + .list(project._id.toString(), { limit: 100 }); if (error) throw error; - if (files && files.length > 0) { - const paths = files.map(f => `${projectId}/${f.name}`); - const { error: delError } = await supabase.storage - .from('dev-files') - .remove(paths); - - if (delError) throw delError; - deletedCount += files.length; - } else { + if (!files || files.length === 0) { hasMore = false; + break; } + + const paths = files.map(f => `${project._id}/${f.name}`); + + const { error: removeError } = await supabase.storage + .from(bucket) + .remove(paths); + + if (removeError) throw removeError; + + deletedCount += files.length; } - res.json({ message: `All files deleted. Total removed: ${deletedCount}` }); + // Reset usage only for internal storage + if (!isExternalStorage(project)) { + project.storageUsed = 0; + await project.save(); + } + + res.json({ + success: true, + deleted: deletedCount, + provider: isExternalStorage(project) ? "external" : "internal" + }); } catch (err) { - res.status(500).json({ error: err.message }); // Fixed + res.status(500).json({ + error: "Failed to delete files", + details: + process.env.NODE_ENV === "development" + ? err.message + : undefined + }); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/models/Project.js b/backend/models/Project.js index 2a8497f22..6b10700b8 100644 --- a/backend/models/Project.js +++ b/backend/models/Project.js @@ -1,5 +1,12 @@ const mongoose = require('mongoose'); +// Encryption schema ko reuse karne ke liye alag se define kiya +const resourceConfigSchema = new mongoose.Schema({ + encrypted: { type: String, select: false }, + iv: { type: String, select: false }, + tag: { type: String, select: false } +}, { _id: false }); + const fieldSchema = new mongoose.Schema({ key: { type: String, required: true }, type: { @@ -15,13 +22,6 @@ 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, @@ -42,18 +42,23 @@ const projectSchema = new mongoose.Schema({ // STORAGE LIMITS (Files) storageUsed: { type: Number, default: 0 }, - storageLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Files + storageLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB default // DATABASE LIMITS (JSON Docs) - databaseUsed: { type: Number, default: 0 }, // - databaseLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB for Data - - externalConfig: { - type: externalConfigSchema, - default: null - }, + databaseUsed: { type: Number, default: 0 }, + databaseLimit: { type: Number, default: 20 * 1024 * 1024 }, // 20MB default - isExternal: { type: Boolean, default: false } + // Granular Resources Structure + resources: { + db: { + isExternal: { type: Boolean, default: false }, + config: { type: resourceConfigSchema, default: null } + }, + storage: { + isExternal: { type: Boolean, default: false }, + config: { type: resourceConfigSchema, default: null } + } + } }, { timestamps: true }); module.exports = mongoose.model('Project', projectSchema); \ No newline at end of file diff --git a/backend/utils/GC.js b/backend/utils/GC.js index 1dfb9b044..a6937e08a 100644 --- a/backend/utils/GC.js +++ b/backend/utils/GC.js @@ -1,4 +1,4 @@ -const registry = require("./registry"); +const { registry } = require("./registry"); function garbageCollect() { setInterval(() => { console.log("20 minutes passed"); diff --git a/backend/utils/connectionManager.js b/backend/utils/connection.manager.js similarity index 56% rename from backend/utils/connectionManager.js rename to backend/utils/connection.manager.js index 8e5d0aa26..94dde7115 100644 --- a/backend/utils/connectionManager.js +++ b/backend/utils/connection.manager.js @@ -1,4 +1,4 @@ -const registry = require("./registry"); +const { registry } = require("./registry"); const Project = require("../models/Project"); const { decrypt } = require("./encryption"); const mongoose = require("mongoose"); @@ -6,6 +6,8 @@ const mongoose = require("mongoose"); async function getConnection(projectId) { const key = projectId.toString(); + + // 1. Registry Check if (registry.has(key)) { const cachedConn = registry.get(key); if (cachedConn.readyState === 1) { @@ -14,37 +16,45 @@ async function getConnection(projectId) { } } + // 2. Fetch Project with HIDDEN fields const projectDoc = await Project.findById(projectId) - .select("+externalConfig.iv +externalConfig.tag +externalConfig.encrypted"); + .select("+resources.db.config.encrypted +resources.db.config.iv +resources.db.config.tag resources.db.isExternal"); + if (!projectDoc) throw new Error("Project not found"); - if (!projectDoc.isExternal) { + + // 3. System DB fallback + if (!projectDoc.resources.db.isExternal) { return mongoose.connection; } + // 4. Decrypt logic let config; try { - const decryptedConfig = decrypt(projectDoc.externalConfig); + // Pura config object pass karo jisme encrypted, iv, aur tag ho + const decryptedConfig = decrypt(projectDoc.resources.db.config); config = JSON.parse(decryptedConfig); - } catch { + } catch (err) { + console.error("Decryption Error:", err); throw new Error("Invalid or corrupted external config"); } + // 5. Create Dynamic Connection const connection = mongoose.createConnection(config.dbUri); connection.on("connected", () => { - console.log(`DB connected for project ${projectId}`); + console.log(`✅ External DB connected: ${projectId}`); }); connection.on("error", (err) => { - console.error("Connection error:", err); + console.error("❌ Connection error [%s]:", projectId, err); + registry.delete(key); // Error hone par cache se hatao }); connection.on("close", () => { registry.delete(key); - console.log(`DB connection closed for project ${key}`); + console.log(`🔌 Connection closed: ${key}`); }); - connection.lastAccessed = new Date(); registry.set(key, connection); diff --git a/backend/utils/input.validation.js b/backend/utils/input.validation.js index b6ca669d2..03d8c4852 100644 --- a/backend/utils/input.validation.js +++ b/backend/utils/input.validation.js @@ -47,4 +47,32 @@ module.exports.sanitize = (obj) => { } } return clean; -}; \ No newline at end of file +}; + +const emptyToUndefined = z.preprocess((val) => (val === "" || val === null ? undefined : val), z.string().optional()); + +module.exports.updateExternalConfigSchema = z.object({ + // DB URI: No .url() check because of mongodb+srv + dbUri: z.preprocess((val) => (val === "" || val === null ? undefined : val), + z.string().optional().refine(val => !val || val.startsWith('mongodb'), { + message: "Invalid Database URI format." + }) + ), + + // Storage URL: Sirf tab check karega jab value empty na ho + storageUrl: z.preprocess((val) => (val === "" || val === null ? undefined : val), + z.string().url("Invalid Storage URL format").optional() + ), + + storageKey: emptyToUndefined, + storageProvider: z.enum(['supabase', 'aws', 'cloudinary']).optional() +}).refine(data => { + // Condition 1: Agar Storage URL diya hai, toh Storage Key bhi honi chahiye + if (data.storageUrl && !data.storageKey) return false; + // Condition 2: Agar Storage Key di hai, toh Storage URL bhi hona chahiye + if (data.storageKey && !data.storageUrl) return false; + // Condition 3: Kam se kam ek cheez (DB ya Storage) poori honi chahiye + return !!(data.dbUri || (data.storageUrl && data.storageKey)); +}, { + message: "Provide either a DB URI or a complete Storage config (URL + Key)." +}); \ No newline at end of file diff --git a/backend/utils/registry.js b/backend/utils/registry.js index 1a3722137..767114268 100644 --- a/backend/utils/registry.js +++ b/backend/utils/registry.js @@ -1,3 +1,3 @@ const registry = new Map(); - -module.exports = registry; +const storageRegistry = new Map(); +module.exports = { registry, storageRegistry }; diff --git a/backend/utils/storage.manager.js b/backend/utils/storage.manager.js new file mode 100644 index 000000000..d43573eb2 --- /dev/null +++ b/backend/utils/storage.manager.js @@ -0,0 +1,60 @@ +const { storageRegistry } = require("./registry"); +const { createClient } = require("@supabase/supabase-js"); +const { decrypt } = require("./encryption"); + +const defaultSupabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_KEY +); + +async function getStorage(project) { + if (!project?._id) { + throw new Error("Project document is required"); + } + + const key = project._id.toString(); + + // Reuse cached client + if (storageRegistry.has(key)) { + const entry = storageRegistry.get(key); + entry.lastUsed = Date.now(); + return entry.client; + } + + let client; + + // Internal storage + if (!project.resources?.storage?.isExternal) { + client = defaultSupabase; + } + // External BYOD storage + else { + try { + const decrypted = decrypt(project.resources.storage.config); + const config = JSON.parse(decrypted); + + if (!config.storageUrl || !config.storageKey) { + throw new Error("Incomplete storage config"); + } + + client = createClient( + config.storageUrl, + config.storageKey + ); + } catch (err) { + console.error("Storage config error:", err); + throw new Error("Invalid storage configuration"); + } + } + + // Register client for pooling + GC + storageRegistry.set(key, { + client, + lastUsed: Date.now(), + isExternal: !!project.resources?.storage?.isExternal + }); + + return client; +} + +module.exports = { getStorage }; diff --git a/frontend/src/pages/Database.jsx b/frontend/src/pages/Database.jsx index fd91b2daf..703e9ee06 100644 --- a/frontend/src/pages/Database.jsx +++ b/frontend/src/pages/Database.jsx @@ -244,7 +244,7 @@ export default function Database() { ); const JsonView = () => ( -
+
                 {JSON.stringify(data, null, 2)}
             
diff --git a/frontend/src/pages/Storage.jsx b/frontend/src/pages/Storage.jsx index d016e74a5..117280eff 100644 --- a/frontend/src/pages/Storage.jsx +++ b/frontend/src/pages/Storage.jsx @@ -60,7 +60,8 @@ export default function Storage() { toast.success("File uploaded!", { id: toastId }); fetchFiles(); } catch (err) { - toast.error("Upload failed", { id: toastId }); + const backendError = err.response?.data?.error || "Upload failed"; + toast.error(backendError, { id: toastId }); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ''; @@ -196,21 +197,6 @@ export default function Storage() { ) : ( )} - - {/* Overlay Actions */} -
- - - - -
{/* Details Area */} @@ -218,12 +204,53 @@ export default function Storage() {
{file.name.split('_').slice(1).join('_') || file.name}
-
+
{formatBytes(file.metadata?.size)} {file.metadata?.mimetype?.split('/')[1] || 'FILE'}
+ + {/* Action Buttons */} +
+ + View + + +
); @@ -232,16 +259,7 @@ export default function Storage() { )} );