Skip to content

Commit ec92d28

Browse files
committed
fix(dashboard-api): add limitFields, populate, cursor pagination, count and structured response to getData
1 parent c05dbc9 commit ec92d28

1 file changed

Lines changed: 6 additions & 57 deletions

File tree

apps/dashboard-api/src/controllers/project.controller.js

Lines changed: 6 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -144,12 +144,6 @@ const getDefaultRlsForCollection = (collectionName, schema = []) => {
144144

145145
const SOCIAL_PROVIDER_KEYS = ["github", "google"];
146146

147-
/**
148-
* Sanitizes authProviders from a project document for safe API responses.
149-
* Strips clientSecret fields and replaces them with a boolean hasClientSecret flag.
150-
* @param {Object} authProviders - Raw authProviders from the project document
151-
* @returns {Object} Sanitized providers keyed by provider name
152-
*/
153147
const sanitizeAuthProviders = (authProviders = {}) => {
154148
return SOCIAL_PROVIDER_KEYS.reduce((acc, provider) => {
155149
const config = authProviders?.[provider] || {};
@@ -336,13 +330,11 @@ module.exports.getAllProject = async (req, res) => {
336330
.select("name description databaseUsed databaseLimit storageUsed storageLimit updatedAt isAuthEnabled collections")
337331
.lean();
338332

339-
// --- HEALTH CALCULATION (SIMULATED / CALCULATED) ---
340-
// Fetch recent log status for all projects to determine health
341333
const projectIds = projects.map(p => p._id);
342334
const recentLogs = await Log.aggregate([
343335
{ $match: { projectId: { $in: projectIds } } },
344336
{ $sort: { timestamp: -1 } },
345-
{ $limit: 100 }, // Get the last 100 logs globally for user to keep it fast
337+
{ $limit: 100 },
346338
{ $group: {
347339
_id: "$projectId",
348340
errorCount: { $sum: { $cond: [{ $gte: ["$status", 400] }, 1, 0] } },
@@ -360,7 +352,6 @@ module.exports.getAllProject = async (req, res) => {
360352
const stats = logsMap[project._id.toString()];
361353
let health = 'healthy';
362354

363-
// Determine health: If > 20% recent errors, mark as warning
364355
if (stats) {
365356
const total = stats.errorCount + stats.successCount;
366357
const errorRate = stats.errorCount / total;
@@ -410,7 +401,6 @@ module.exports.getSingleProject = async (req, res) => {
410401
await setProjectById(req.params.projectId, projectObj);
411402
}
412403

413-
// Ownership Check (Even for Cache)
414404
if (projectObj.owner.toString() !== req.user._id.toString()) {
415405
return res.status(403).json({ error: "Access denied." });
416406
}
@@ -423,7 +413,7 @@ module.exports.getSingleProject = async (req, res) => {
423413

424414
module.exports.regenerateApiKey = async (req, res) => {
425415
try {
426-
const { keyType } = req.body; // 'publishable' or 'secret'
416+
const { keyType } = req.body;
427417

428418
if (keyType !== "publishable" && keyType !== "secret") {
429419
return res
@@ -442,7 +432,6 @@ module.exports.regenerateApiKey = async (req, res) => {
442432
if (!oldApiProj)
443433
return res.status(404).json({ error: "Project not found." });
444434

445-
// CLEAR CACHE
446435
await deleteProjectByApiKeyCache(oldApiProj.publishableKey);
447436
await deleteProjectByApiKeyCache(oldApiProj.secretKey);
448437

@@ -481,7 +470,7 @@ const dropCollectionIfExists = async (connection, collectionName) => {
481470
}
482471
}
483472
};
484-
// VALIDATE URI
473+
485474
const isSafeUri = (uri) => {
486475
try {
487476
const parsed = new URL(uri);
@@ -497,13 +486,11 @@ module.exports.updateExternalConfig = async (req, res) => {
497486
try {
498487
const { projectId } = req.params;
499488

500-
// POST FOR - EXTERNAL CONFIG
501489
const validatedData = updateExternalConfigSchema.parse(req.body);
502490
const { dbUri, storageUrl, storageKey, storageProvider } = validatedData;
503491

504492
const updateData = {};
505493

506-
// DB CONFIG
507494
if (dbUri) {
508495
if (!isSafeUri(dbUri))
509496
return res.status(400).json({
@@ -514,7 +501,6 @@ module.exports.updateExternalConfig = async (req, res) => {
514501
updateData["resources.db.config"] = encrypt(JSON.stringify({ dbUri }));
515502
updateData["resources.db.isExternal"] = true;
516503

517-
// --- VERIFY CONNECTION ---
518504
console.log("Verifying connection to:", projectId);
519505
try {
520506
const tempConn = mongoose.createConnection(dbUri, {
@@ -538,10 +524,8 @@ module.exports.updateExternalConfig = async (req, res) => {
538524

539525
return res.status(400).json({ error: errorMsg });
540526
}
541-
// -------------------------
542527
}
543528

544-
// STORAGE CONFIG
545529
if (storageUrl && storageKey) {
546530
const storageConfig = {
547531
storageUrl,
@@ -641,7 +625,6 @@ module.exports.deleteExternalStorageConfig = async (req, res) => {
641625
}
642626
};
643627

644-
// POST REQ FOR CREATE COLLECTION
645628
module.exports.createCollection = async (req, res) => {
646629
const executeOperation = async (session) => {
647630
const { projectId, collectionName, schema } = createCollectionSchema.parse(req.body);
@@ -775,7 +758,7 @@ module.exports.createCollection = async (req, res) => {
775758
}
776759
};
777760

778-
// GET DOC BY ID
761+
// GET DOC BY ID — FIXED: added limitFields(), populate(), cursor pagination, count support, structured response
779762
module.exports.getData = async (req, res) => {
780763
try {
781764
const { projectId, collectionName } = req.params;
@@ -1061,7 +1044,6 @@ module.exports.editRow = async (req, res) => {
10611044

10621045
if (collectionName === "users") {
10631046
delete req.body.password;
1064-
// Also ensure it's not and nested or sneaky
10651047
Object.keys(req.body).forEach((key) => {
10661048
if (key.toLowerCase().includes("password")) delete req.body[key];
10671049
});
@@ -1293,7 +1275,6 @@ module.exports.requestUpload = async (req, res, next) => {
12931275

12941276
const external = isProjectStorageExternal(project);
12951277

1296-
// Pre-check quota only; actual storage usage is charged after confirmUpload verifies object existence and size.
12971278
if (!external) {
12981279
const storageLimit =
12991280
typeof project.storageLimit === "number"
@@ -1353,12 +1334,10 @@ module.exports.confirmUpload = async (req, res, next) => {
13531334
const external = isProjectStorageExternal(project);
13541335
const normalizedPath = normalizeProjectPath(projectId, sanitizedFilePath);
13551336

1356-
// make sure client isn't confirming someone else's file
13571337
if (!normalizedPath) {
13581338
return next(new AppError(403, "Access denied."));
13591339
}
13601340

1361-
// verify file actually exists on cloud before touching quota
13621341
let actualSize;
13631342
try {
13641343
actualSize = await verifyUploadedFile(project, normalizedPath);
@@ -1387,7 +1366,6 @@ module.exports.confirmUpload = async (req, res, next) => {
13871366
);
13881367
}
13891368

1390-
// now it's safe to charge quota
13911369
if (!external) {
13921370
const result = await Project.updateOne(
13931371
{
@@ -1502,7 +1480,6 @@ module.exports.updateProject = async (req, res) => {
15021480
.json({ error: "resendApiKey must be a non-empty string." });
15031481
}
15041482

1505-
// Sanitize the key: Prevent CRLF (HTTP Header Injection) and invalid characters
15061483
if (!/^re_[A-Za-z0-9_]+$/.test(trimmedKey)) {
15071484
return res.status(400).json({ error: "Invalid Resend API Key format." });
15081485
}
@@ -1551,7 +1528,6 @@ module.exports.listMailTemplates = async (req, res, next) => {
15511528
try {
15521529
const { projectId } = req.params;
15531530

1554-
// Load as document so we can migrate legacy embedded templates if present
15551531
const project = await Project.findOne({ _id: projectId, owner: req.user._id }).select("+mailTemplates");
15561532

15571533
if (!project) return res.status(404).json({ success: false, data: {}, message: "Project not found." });
@@ -1635,7 +1611,6 @@ module.exports.listMailTemplates = async (req, res, next) => {
16351611
}
16361612

16371613
if (migrationSafeToFinalize) {
1638-
// Clear legacy embedded templates only after migration writes are complete.
16391614
project.mailTemplates = [];
16401615
await project.save();
16411616
await deleteProjectById(project._id.toString()).catch(() => {});
@@ -1671,7 +1646,6 @@ module.exports.listGlobalMailTemplates = async (req, res, next) => {
16711646
try {
16721647
const { projectId } = req.params;
16731648

1674-
// Keep auth consistent: only show to project owners
16751649
const project = await Project.findOne({ _id: projectId, owner: req.user._id })
16761650
.select("_id")
16771651
.lean();
@@ -1723,7 +1697,6 @@ module.exports.getMailTemplate = async (req, res, next) => {
17231697
.select("_id key name subject html text updatedAt projectId isSystem")
17241698
.lean();
17251699

1726-
// Legacy fallback (should be rare after listMailTemplates migration)
17271700
if (!template) {
17281701
const legacy = Array.isArray(project.mailTemplates) ? project.mailTemplates : [];
17291702
const lt = legacy.find((x) => String(x._id) === String(templateId));
@@ -2007,7 +1980,6 @@ module.exports.deleteProject = async (req, res) => {
20071980
.json({ error: "Project not found or access denied." });
20081981
}
20091982

2010-
// DROP COLLECTIONS: Only for internal databases
20111983
if (!project.resources.db.isExternal) {
20121984
for (const col of project.collections) {
20131985
const collectionName = `${project._id}_${col.name}`;
@@ -2021,7 +1993,6 @@ module.exports.deleteProject = async (req, res) => {
20211993
} catch (e) {}
20221994
}
20231995

2024-
// DELETE: Only for internal Infraa
20251996
if (!isProjectStorageExternal(project)) {
20261997
const supabase = await getStorage(project);
20271998
const bucket = getBucket(project);
@@ -2057,7 +2028,6 @@ module.exports.deleteProject = async (req, res) => {
20572028
}
20582029
};
20592030

2060-
// ENRICHED analytics function for the premium dashboard
20612031
module.exports.analytics = async (req, res, next) => {
20622032
try {
20632033
const { projectId } = req.params;
@@ -2089,7 +2059,7 @@ module.exports.analytics = async (req, res, next) => {
20892059
case 'last1h':
20902060
startDate.setHours(startDate.getHours() - 1);
20912061
format = "%H:%M";
2092-
groupStep = "minute"; // We'll group by minute for 1h
2062+
groupStep = "minute";
20932063
break;
20942064
case 'last24h':
20952065
startDate.setDate(startDate.getDate() - 1);
@@ -2112,7 +2082,6 @@ module.exports.analytics = async (req, res, next) => {
21122082
timestamp: { $gte: startDate },
21132083
};
21142084

2115-
// 1. Aggregation for Time Series (Requests & Latency)
21162085
const timeSeriesData = await ApiAnalytics.aggregate([
21172086
{ $match: match },
21182087
{
@@ -2126,7 +2095,6 @@ module.exports.analytics = async (req, res, next) => {
21262095
{ $sort: { _id: 1 } }
21272096
]);
21282097

2129-
// 2. Aggregation for Breakdowns (Status, Method, Top Endpoints)
21302098
const [breakdownStats, topEndpoints] = await Promise.all([
21312099
ApiAnalytics.aggregate([
21322100
{ $match: match },
@@ -2169,7 +2137,6 @@ module.exports.analytics = async (req, res, next) => {
21692137
const stats = breakdownStats[0].global[0] || { avgResponseTimeMs: 0, totalRequests: 0, errors: 0 };
21702138
const errorRate = stats.totalRequests > 0 ? (stats.errors / stats.totalRequests) * 100 : 0;
21712139

2172-
// 3. Approximate p95
21732140
let p95 = 0;
21742141
if (stats.totalRequests > 0) {
21752142
const p95Results = await ApiAnalytics.find(match)
@@ -2181,11 +2148,9 @@ module.exports.analytics = async (req, res, next) => {
21812148
p95 = p95Results[0]?.responseTimeMs || 0;
21822149
}
21832150

2184-
// 4. Logs (last 50)
21852151
const rawLogs = await ApiAnalytics.find(match).sort({ timestamp: -1 }).limit(50).lean();
21862152
const logs = rawLogs.map(l => ({ ...l, path: l.endpoint, status: l.statusCode }));
21872153

2188-
// Cumulative stats for the project
21892154
const allTimeRequests = await Log.countDocuments({ projectId });
21902155

21912156
return res.json({
@@ -2223,16 +2188,11 @@ module.exports.analytics = async (req, res, next) => {
22232188
}
22242189
};
22252190

2226-
// FUNCTION - TOGGLE AUTH
22272191
module.exports.toggleAuth = async (req, res) => {
22282192
try {
22292193
const { projectId } = req.params;
2230-
const { enable } = req.body; // true or false
2194+
const { enable } = req.body;
22312195

2232-
// Ensure user owns project, and load authProviders secrets so sanitizeAuthProviders
2233-
// can correctly compute hasClientSecret in the response.
2234-
// NOTE: If new OAuth providers are added to SOCIAL_PROVIDER_KEYS, extend this select list
2235-
// to include their clientSecret fields as well.
22362196
const project = await Project.findOne({
22372197
_id: projectId,
22382198
owner: req.user._id,
@@ -2284,11 +2244,6 @@ module.exports.toggleAuth = async (req, res) => {
22842244
}
22852245
};
22862246

2287-
/**
2288-
* Updates GitHub/Google OAuth provider settings for a project.
2289-
* Preserves existing encrypted client secrets when not provided in the update.
2290-
* @route PUT /api/projects/:projectId/auth-providers
2291-
*/
22922247
module.exports.updateAuthProviders = async (req, res) => {
22932248
try {
22942249
const { projectId } = req.params;
@@ -2331,7 +2286,6 @@ module.exports.updateAuthProviders = async (req, res) => {
23312286
});
23322287
}
23332288

2334-
// P1: Require siteUrl before enabling any OAuth provider
23352289
if (nextEnabled && !project.siteUrl?.trim()) {
23362290
return res.status(422).json({
23372291
error: "siteUrl required",
@@ -2365,8 +2319,6 @@ module.exports.updateAuthProviders = async (req, res) => {
23652319
}
23662320
};
23672321

2368-
2369-
// PATCH FOR UPDATING COLLECTION RLS
23702322
module.exports.updateCollectionRls = async (req, res) => {
23712323
try {
23722324
const { projectId, collectionName } = req.params;
@@ -2404,7 +2356,6 @@ module.exports.updateCollectionRls = async (req, res) => {
24042356
});
24052357
}
24062358

2407-
// Restrict use of '_id' as ownerField to the 'users' collection only.
24082359
if (nextOwnerField === '_id' && collection.name !== 'users') {
24092360
return res.status(400).json({
24102361
error: "Invalid owner field",
@@ -2632,7 +2583,6 @@ module.exports.deleteContact = async (req, res) => {
26322583
const safeAudienceId = encodeURIComponent(audienceId);
26332584
const safeContactId = encodeURIComponent(contactId);
26342585

2635-
// Resend uses DELETE /audiences/{audience_id}/contacts/{id} or by email
26362586
await axios.delete(`https://api.resend.com/audiences/${safeAudienceId}/contacts/${safeContactId}`, {
26372587
headers: { Authorization: `Bearer ${key}` }
26382588
});
@@ -2667,7 +2617,6 @@ module.exports.sendMarketingBroadcast = async (req, res) => {
26672617
return res.status(400).json({ success: false, message: "Audience ID, subject, and html content are required." });
26682618
}
26692619

2670-
// Mass marketing broadcasts logic using Resend Broadcasts API
26712620
const payload = {
26722621
audience_id: audienceId,
26732622
subject,

0 commit comments

Comments
 (0)