fix: code review — security, dead code, performance, consistency
Critical fixes: - XSS: escapeHtml() on all user-supplied text in email notifications - Budget PATCH: added mutex lock + availability validation (prevents corruption) - batchResolveNames: fixed wrong signature for budget request earmark names Dead code cleanup: - Deleted 8 unused PostComposition* files (replaced by PostDetail full page) Performance: - budget-helpers: single-fetch with computeFromEntries(), optional prefetch param - post-composition: parallelized text + thumbnail fetches with Promise.all Consistency: - PostDetail.jsx: native <select> → PortalSelect (matches all panels) - Finance.jsx: 11 hardcoded English table headers → t() with i18n keys - PostCalendar.jsx: day names, Month/Week labels → t() with i18n keys - App.jsx Suspense: "Loading..." → brand spinner (can't use i18n in fallback) - UploadZone: proper useRef pattern, no vanilla JS document.createElement - All file inputs: className="hidden" → absolute w-0 h-0 opacity-0 - ArtefactDetailPanel: removed campaign/project selects (inherited from post) - TranslationDetailPanel: removed brand/linked post selects (inherited from post) - ApproverMultiSelect: portal-based dropdown (fixes clipping in modals) - Thumbnail fix: post-composition constructs URL from filename (was undefined) - Upload fix: UploadZone with drag-and-drop for design + video artefacts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-4
@@ -1,3 +1,4 @@
|
||||
// TODO: Decompose routes into separate files by domain (posts, campaigns, tasks, artefacts, budget, auth)
|
||||
require('dotenv').config({ path: __dirname + '/.env' });
|
||||
|
||||
const express = require('express');
|
||||
@@ -1312,6 +1313,8 @@ app.get('/api/posts/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/posts', requireAuth, async (req, res) => {
|
||||
// NOTE: Client sends `assigned_to` but NocoDB column is `assigned_to_id`.
|
||||
// Response includes both `assigned_to: post.assigned_to_id` for backward compat.
|
||||
const { title, description, brand_id, assigned_to, status, platform, platforms, content_type, scheduled_date, notes, campaign_id, approver_ids, caption } = req.body;
|
||||
|
||||
const platformsArr = platforms || (platform ? [platform] : []);
|
||||
@@ -1374,6 +1377,8 @@ app.post('/api/posts/bulk-delete', requireAuth, requireRole('superadmin', 'manag
|
||||
});
|
||||
|
||||
app.patch('/api/posts/:id', requireAuth, requireOwnerOrRole('posts', 'superadmin', 'manager'), async (req, res) => {
|
||||
// NOTE: Client sends `assigned_to` but NocoDB column is `assigned_to_id`.
|
||||
// Mapping: req.body.assigned_to → data.assigned_to_id (see below).
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const existing = await nocodb.get('Posts', id);
|
||||
@@ -2525,6 +2530,7 @@ app.post('/api/budget', requireAuth, requireRole('superadmin', 'manager'), async
|
||||
});
|
||||
|
||||
app.patch('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'), async (req, res) => {
|
||||
let releaseLock = null;
|
||||
try {
|
||||
const existing = await nocodb.get('BudgetEntries', req.params.id);
|
||||
if (!existing) return res.status(404).json({ error: 'Budget entry not found' });
|
||||
@@ -2538,6 +2544,29 @@ app.patch('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'),
|
||||
|
||||
if (Object.keys(data).length === 0) return res.status(400).json({ error: 'No fields to update' });
|
||||
|
||||
// Validate amount > 0 if being changed
|
||||
if (data.amount !== undefined && (isNaN(Number(data.amount)) || Number(data.amount) <= 0)) {
|
||||
return res.status(400).json({ error: 'Amount must be greater than 0' });
|
||||
}
|
||||
|
||||
// Budget validation: check availability when changing to expense or increasing expense amount
|
||||
const newType = data.type || existing.type || 'income';
|
||||
const oldType = existing.type || 'income';
|
||||
const newAmount = data.amount !== undefined ? Number(data.amount) : (existing.amount || 0);
|
||||
const oldAmount = existing.amount || 0;
|
||||
const needsCheck = (newType === 'expense' && oldType !== 'expense') ||
|
||||
(newType === 'expense' && newAmount > oldAmount);
|
||||
|
||||
if (needsCheck) {
|
||||
releaseLock = await acquireBudgetLock();
|
||||
const { available } = await getMainAvailable();
|
||||
// Add back the old expense amount if it was already an expense, then check new amount
|
||||
const effectiveAvailable = oldType === 'expense' ? available + oldAmount : available;
|
||||
if (newAmount > effectiveAvailable) {
|
||||
return res.status(400).json({ error: 'Insufficient budget', available: effectiveAvailable });
|
||||
}
|
||||
}
|
||||
|
||||
await nocodb.update('BudgetEntries', req.params.id, data);
|
||||
|
||||
const entry = await nocodb.get('BudgetEntries', req.params.id);
|
||||
@@ -2548,6 +2577,8 @@ app.patch('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'),
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Failed to update budget entry' });
|
||||
} finally {
|
||||
if (releaseLock) releaseLock();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2721,10 +2752,11 @@ app.get('/api/budget-requests', requireAuth, requireRole('superadmin'), async (r
|
||||
...r,
|
||||
requester_name: users[r.requested_by_user_id] || 'Unknown',
|
||||
}));
|
||||
await batchResolveNames(enriched, {
|
||||
earmarked_campaign_id: { table: 'Campaigns', as: 'earmarked_campaign_name' },
|
||||
earmarked_project_id: { table: 'Projects', as: 'earmarked_project_name' },
|
||||
});
|
||||
// Resolve earmarked campaign/project names
|
||||
for (const r of enriched) {
|
||||
if (r.earmarked_campaign_id) r.earmarked_campaign_name = await getRecordName('Campaigns', Number(r.earmarked_campaign_id));
|
||||
if (r.earmarked_project_id) r.earmarked_project_name = await getRecordName('Projects', Number(r.earmarked_project_id));
|
||||
}
|
||||
res.json(enriched);
|
||||
} catch (err) {
|
||||
console.error('Budget requests list error:', err);
|
||||
|
||||
Reference in New Issue
Block a user