Campaign assignments, ownership-based editing, and role-scoped data

- Add campaign_assignments table for user-to-campaign mapping
- Superadmin/managers can assign users to campaigns; visibility filtered by assignment/ownership
- Managers can only manage (tracks, assignments) on campaigns they created
- Budget controlled by superadmin only, with proper modal UI for editing
- Ownership-based editing for campaigns, projects, comments (creators can edit their own)
- Role-scoped dashboard and finance data (managers see only their campaigns' data)
- Manager's budget derived from sum of their campaign budgets set by superadmin
- Hide UI features users cannot use (principle of least privilege across all pages)
- Fix profile completion prompt persisting after saving (login response now includes profileComplete)
- Add post detail modal in campaign detail with thumbnails, publication links, and metadata
- Add comment inline editing for comment authors
- Move financial summary cards below filters on Campaigns page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-02-09 13:59:40 +03:00
parent 9b58e5e9aa
commit d15e54044e
11 changed files with 797 additions and 154 deletions

View File

@@ -178,6 +178,18 @@ function initialize() {
);
`);
// Campaign assignments (user-to-campaign junction table)
db.exec(`
CREATE TABLE IF NOT EXISTS campaign_assignments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
assigned_by INTEGER REFERENCES users(id),
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(campaign_id, user_id)
);
`);
// ─── Column migrations ───
// Helper: adds a column to a table if it does not already exist.
function addColumnIfMissing(table, column, definition) {

View File

@@ -143,7 +143,7 @@ function requireRole(...roles) {
}
// Ownership check: contributors can only modify their own resources (or resources assigned to them)
const VALID_OWNER_TABLES = new Set(['posts', 'tasks']);
const VALID_OWNER_TABLES = new Set(['posts', 'tasks', 'campaigns', 'projects']);
function requireOwnerOrRole(table, ...allowedRoles) {
if (!VALID_OWNER_TABLES.has(table)) {
@@ -158,7 +158,7 @@ function requireOwnerOrRole(table, ...allowedRoles) {
return next();
}
// Contributors must own the resource or be assigned to it
const row = db.prepare(`SELECT created_by_user_id, assigned_to FROM ${table} WHERE id = ?`).get(req.params.id);
const row = db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(req.params.id);
if (!row) {
return res.status(404).json({ error: 'Not found' });
}
@@ -166,10 +166,12 @@ function requireOwnerOrRole(table, ...allowedRoles) {
if (row.created_by_user_id === req.session.userId) {
return next();
}
// Check if resource is assigned to user's team member
const currentUser = db.prepare('SELECT team_member_id FROM users WHERE id = ?').get(req.session.userId);
if (currentUser?.team_member_id && row.assigned_to === currentUser.team_member_id) {
return next();
// Check if resource is assigned to user's team member (for tables with assigned_to)
if (row.assigned_to !== undefined) {
const currentUser = db.prepare('SELECT team_member_id FROM users WHERE id = ?').get(req.session.userId);
if (currentUser?.team_member_id && row.assigned_to === currentUser.team_member_id) {
return next();
}
}
return res.status(403).json({ error: 'You can only modify your own items' });
};
@@ -201,6 +203,7 @@ app.post('/api/auth/login', async (req, res) => {
req.session.userName = user.name;
req.session.teamMemberId = user.team_member_id;
const profileComplete = !!user.team_role;
res.json({
user: {
id: user.id,
@@ -209,6 +212,9 @@ app.post('/api/auth/login', async (req, res) => {
role: user.role,
avatar: user.avatar,
team_member_id: user.team_member_id,
team_role: user.team_role,
tutorial_completed: user.tutorial_completed,
profileComplete,
},
});
} catch (err) {
@@ -233,7 +239,7 @@ app.get('/api/auth/me', requireAuth, (req, res) => {
return res.status(404).json({ error: 'User not found' });
}
// Check if profile is complete
const profileComplete = !!(user.team_role && user.brands);
const profileComplete = !!user.team_role;
res.json({ ...user, profileComplete, brands: JSON.parse(user.brands || '[]') });
});
@@ -386,6 +392,8 @@ app.get('/api/auth/permissions', requireAuth, (req, res) => {
canManageFinance: canManage,
canManageTeam: canManage,
canManageUsers: role === 'superadmin',
canAssignCampaigns: canManage,
canSetBudget: role === 'superadmin',
// Posts & tasks: everyone can create, but only own (for contributors)
canCreatePosts: true,
canCreateTasks: true,
@@ -418,12 +426,15 @@ app.get('/api/users/team', requireAuth, (req, res) => {
ORDER BY name
`).all();
// Filter based on brand overlap for non-superadmins
// Skip brand filter when loading users for campaign assignment (managers need to see all users)
const skipBrandFilter = req.query.all === 'true' && (req.session.userRole === 'superadmin' || req.session.userRole === 'manager');
// Filter based on brand overlap for non-superadmins (unless skipped)
let filteredUsers = users;
if (req.session.userRole !== 'superadmin') {
if (req.session.userRole !== 'superadmin' && !skipBrandFilter) {
const currentUser = db.prepare('SELECT brands FROM users WHERE id = ?').get(req.session.userId);
const myBrands = JSON.parse(currentUser?.brands || '[]');
filteredUsers = users.filter(u => {
const theirBrands = JSON.parse(u.brands || '[]');
// Always include self, or if there's brand overlap
@@ -929,10 +940,27 @@ app.delete('/api/assets/:id', requireAuth, requireRole('superadmin', 'manager'),
app.get('/api/campaigns', requireAuth, (req, res) => {
const { brand_id, status, start_date, end_date } = req.query;
let sql = CAMPAIGN_SELECT_SQL;
const isSuperadmin = req.session.userRole === 'superadmin';
let sql;
if (isSuperadmin) {
sql = CAMPAIGN_SELECT_SQL;
} else {
// Non-superadmins only see campaigns assigned to them or created by them
sql = `SELECT DISTINCT c.*, b.name as brand_name
FROM campaigns c
LEFT JOIN brands b ON c.brand_id = b.id
LEFT JOIN campaign_assignments ca ON ca.campaign_id = c.id`;
}
const conditions = [];
const values = [];
if (!isSuperadmin) {
conditions.push('(c.created_by_user_id = ? OR ca.user_id = ?)');
values.push(req.session.userId, req.session.userId);
}
if (brand_id) { conditions.push('c.brand_id = ?'); values.push(brand_id); }
if (status) { conditions.push('c.status = ?'); values.push(status); }
if (start_date) { conditions.push('c.end_date >= ?'); values.push(start_date); }
@@ -945,30 +973,55 @@ app.get('/api/campaigns', requireAuth, (req, res) => {
res.json(campaigns.map(c => ({ ...c, platforms: JSON.parse(c.platforms || '[]') })));
});
app.get('/api/campaigns/:id', requireAuth, (req, res) => {
const campaign = db.prepare(`${CAMPAIGN_SELECT_SQL} WHERE c.id = ?`).get(req.params.id);
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
if (!userHasCampaignAccess(req.session.userId, req.session.userRole, req.params.id)) {
return res.status(403).json({ error: 'You do not have access to this campaign' });
}
res.json({ ...campaign, platforms: JSON.parse(campaign.platforms || '[]') });
});
app.post('/api/campaigns', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
const { name, description, brand_id, start_date, end_date, status, color, budget, goals, platforms } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
if (!start_date || !end_date) return res.status(400).json({ error: 'Start and end dates are required' });
// Managers cannot set budget — only superadmin can
const effectiveBudget = req.session.userRole === 'superadmin' ? (budget || null) : null;
const result = db.prepare(`
INSERT INTO campaigns (name, description, brand_id, start_date, end_date, status, color, budget, goals, platforms, created_by_user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(name, description || null, brand_id || null, start_date, end_date, status || 'planning', color || null, budget || null, goals || null, JSON.stringify(platforms || []), req.session.userId);
`).run(name, description || null, brand_id || null, start_date, end_date, status || 'planning', color || null, effectiveBudget, goals || null, JSON.stringify(platforms || []), req.session.userId);
const campaign = db.prepare(`${CAMPAIGN_SELECT_SQL} WHERE c.id = ?`).get(result.lastInsertRowid);
const campaignId = result.lastInsertRowid;
// Auto-assign creator to campaign
db.prepare('INSERT OR IGNORE INTO campaign_assignments (campaign_id, user_id, assigned_by) VALUES (?, ?, ?)').run(campaignId, req.session.userId, req.session.userId);
const campaign = db.prepare(`${CAMPAIGN_SELECT_SQL} WHERE c.id = ?`).get(campaignId);
res.status(201).json({ ...campaign, platforms: JSON.parse(campaign.platforms || '[]') });
});
app.patch('/api/campaigns/:id', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
app.patch('/api/campaigns/:id', requireAuth, requireOwnerOrRole('campaigns', 'superadmin', 'manager'), (req, res) => {
const { id } = req.params;
const existing = db.prepare('SELECT * FROM campaigns WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ error: 'Campaign not found' });
// Strip budget field if user is not superadmin
const body = { ...req.body };
if (req.session.userRole !== 'superadmin') {
delete body.budget;
}
const campaignFields = [
'name', 'description', 'brand_id', 'start_date', 'end_date', 'status', 'color', 'budget', 'goals',
'budget_spent', 'revenue', 'impressions', 'clicks', 'conversions', 'cost_per_click', 'notes', 'platforms',
];
const { clauses, values, hasUpdates } = buildUpdate(req.body, campaignFields, { jsonFields: ['platforms'] });
const { clauses, values, hasUpdates } = buildUpdate(body, campaignFields, { jsonFields: ['platforms'] });
if (!hasUpdates) return res.status(400).json({ error: 'No fields to update' });
@@ -1003,13 +1056,93 @@ app.delete('/api/campaigns/:id', requireAuth, requireRole('superadmin', 'manager
}
});
// ─── CAMPAIGN ASSIGNMENTS ───────────────────────────────────────
// Helper: check if user has access to a campaign
function userHasCampaignAccess(userId, userRole, campaignId) {
if (userRole === 'superadmin') return true;
const row = db.prepare(`
SELECT 1 FROM campaigns c
LEFT JOIN campaign_assignments ca ON ca.campaign_id = c.id AND ca.user_id = ?
WHERE c.id = ? AND (c.created_by_user_id = ? OR ca.user_id = ?)
`).get(userId, campaignId, userId, userId);
return !!row;
}
app.get('/api/campaigns/:id/assignments', requireAuth, (req, res) => {
const assignments = db.prepare(`
SELECT ca.*, u.name as user_name, u.email as user_email, u.avatar as user_avatar, u.role as user_role,
ab.name as assigned_by_name
FROM campaign_assignments ca
JOIN users u ON ca.user_id = u.id
LEFT JOIN users ab ON ca.assigned_by = ab.id
WHERE ca.campaign_id = ?
ORDER BY ca.assigned_at ASC
`).all(req.params.id);
res.json(assignments);
});
app.post('/api/campaigns/:id/assignments', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
const { user_ids } = req.body;
if (!Array.isArray(user_ids) || user_ids.length === 0) {
return res.status(400).json({ error: 'user_ids array is required' });
}
const campaign = db.prepare('SELECT id, created_by_user_id FROM campaigns WHERE id = ?').get(req.params.id);
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
// Only superadmin or campaign creator can assign
if (req.session.userRole !== 'superadmin' && campaign.created_by_user_id !== req.session.userId) {
return res.status(403).json({ error: 'Only the campaign creator or superadmin can assign members' });
}
const insert = db.prepare('INSERT OR IGNORE INTO campaign_assignments (campaign_id, user_id, assigned_by) VALUES (?, ?, ?)');
const tx = db.transaction(() => {
for (const userId of user_ids) {
insert.run(req.params.id, userId, req.session.userId);
}
});
tx();
// Return updated assignments
const assignments = db.prepare(`
SELECT ca.*, u.name as user_name, u.email as user_email, u.avatar as user_avatar, u.role as user_role
FROM campaign_assignments ca
JOIN users u ON ca.user_id = u.id
WHERE ca.campaign_id = ?
ORDER BY ca.assigned_at ASC
`).all(req.params.id);
res.json(assignments);
});
app.delete('/api/campaigns/:id/assignments/:userId', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
const campaign = db.prepare('SELECT id, created_by_user_id FROM campaigns WHERE id = ?').get(req.params.id);
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
// Only superadmin or campaign creator can unassign
if (req.session.userRole !== 'superadmin' && campaign.created_by_user_id !== req.session.userId) {
return res.status(403).json({ error: 'Only the campaign creator or superadmin can remove members' });
}
const result = db.prepare('DELETE FROM campaign_assignments WHERE campaign_id = ? AND user_id = ?').run(req.params.id, req.params.userId);
if (result.changes === 0) return res.status(404).json({ error: 'Assignment not found' });
res.json({ success: true });
});
// ─── BUDGET ENTRIES ─────────────────────────────────────────────
app.get('/api/budget', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
const isSuperadmin = req.session.userRole === 'superadmin';
const userId = req.session.userId;
const campaignFilter = isSuperadmin
? ''
: `AND be.campaign_id IN (SELECT id FROM campaigns WHERE created_by_user_id = ${userId} UNION SELECT campaign_id FROM campaign_assignments WHERE user_id = ${userId})`;
const entries = db.prepare(`
SELECT be.*, c.name as campaign_name
FROM budget_entries be
LEFT JOIN campaigns c ON be.campaign_id = c.id
WHERE 1=1 ${campaignFilter}
ORDER BY be.date_received DESC
`).all();
res.json(entries);
@@ -1050,10 +1183,23 @@ app.delete('/api/budget/:id', requireAuth, requireRole('superadmin', 'manager'),
res.json({ success: true });
});
// Finance summary — aggregates across all campaigns & tracks
// Finance summary — aggregates across campaigns & tracks (scoped by user's campaigns)
app.get('/api/finance/summary', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
const totalReceived = db.prepare('SELECT COALESCE(SUM(amount), 0) as total FROM budget_entries').get().total;
const isSuperadmin = req.session.userRole === 'superadmin';
const userId = req.session.userId;
const myCampaignIds = isSuperadmin
? null
: `SELECT id FROM campaigns WHERE created_by_user_id = ${userId} UNION SELECT campaign_id FROM campaign_assignments WHERE user_id = ${userId}`;
// For superadmin: totalReceived from budget_entries (org-wide funding)
// For managers: totalReceived = sum of their campaigns' budgets (allocated by superadmin)
const totalReceived = isSuperadmin
? db.prepare('SELECT COALESCE(SUM(amount), 0) as total FROM budget_entries').get().total
: db.prepare(`SELECT COALESCE(SUM(budget), 0) as total FROM campaigns WHERE id IN (${myCampaignIds})`).get().total;
const campaignFilter = isSuperadmin
? ''
: `WHERE c.id IN (${myCampaignIds})`;
const campaignStats = db.prepare(`
SELECT
c.id, c.name, c.budget, c.status,
@@ -1065,6 +1211,7 @@ app.get('/api/finance/summary', requireAuth, requireRole('superadmin', 'manager'
COALESCE(SUM(ct.conversions), 0) as tracks_conversions
FROM campaigns c
LEFT JOIN campaign_tracks ct ON ct.campaign_id = c.id
${campaignFilter}
GROUP BY c.id
ORDER BY c.start_date DESC
`).all();
@@ -1090,6 +1237,9 @@ app.get('/api/finance/summary', requireAuth, requireRole('superadmin', 'manager'
// ─── CAMPAIGN TRACKS ────────────────────────────────────────────
app.get('/api/campaigns/:id/tracks', requireAuth, (req, res) => {
if (!userHasCampaignAccess(req.session.userId, req.session.userRole, req.params.id)) {
return res.status(403).json({ error: 'You do not have access to this campaign' });
}
const tracks = db.prepare('SELECT * FROM campaign_tracks WHERE campaign_id = ? ORDER BY created_at').all(req.params.id);
res.json(tracks);
});
@@ -1133,16 +1283,25 @@ app.delete('/api/tracks/:id', requireAuth, requireRole('superadmin', 'manager'),
// Get posts linked to a campaign (across all tracks)
app.get('/api/campaigns/:id/posts', requireAuth, (req, res) => {
if (!userHasCampaignAccess(req.session.userId, req.session.userRole, req.params.id)) {
return res.status(403).json({ error: 'You do not have access to this campaign' });
}
const posts = db.prepare(`
SELECT p.*, t.name as assigned_name, b.name as brand_name, ct.name as track_name, ct.type as track_type
SELECT p.*, t.name as assigned_name, b.name as brand_name, ct.name as track_name, ct.type as track_type,
u.name as creator_user_name
FROM posts p
LEFT JOIN team_members t ON p.assigned_to = t.id
LEFT JOIN brands b ON p.brand_id = b.id
LEFT JOIN campaign_tracks ct ON p.track_id = ct.id
LEFT JOIN users u ON p.created_by_user_id = u.id
WHERE p.campaign_id = ?
ORDER BY p.created_at DESC
`).all(req.params.id);
res.json(posts.map(parsePostJson));
const thumbnailStmt = db.prepare("SELECT url FROM post_attachments WHERE post_id = ? AND mime_type LIKE 'image/%' ORDER BY created_at ASC LIMIT 1");
res.json(posts.map(p => ({
...parsePostJson(p),
thumbnail_url: thumbnailStmt.get(p.id)?.url || null,
})));
});
// ─── PROJECTS ───────────────────────────────────────────────────
@@ -1182,7 +1341,7 @@ app.post('/api/projects', requireAuth, requireRole('superadmin', 'manager'), (re
res.status(201).json(project);
});
app.patch('/api/projects/:id', requireAuth, requireRole('superadmin', 'manager'), (req, res) => {
app.patch('/api/projects/:id', requireAuth, requireOwnerOrRole('projects', 'superadmin', 'manager'), (req, res) => {
const { id } = req.params;
const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
if (!existing) return res.status(404).json({ error: 'Project not found' });
@@ -1303,25 +1462,36 @@ app.delete('/api/tasks/:id', requireAuth, requireOwnerOrRole('tasks', 'superadmi
// ─── DASHBOARD ──────────────────────────────────────────────────
app.get('/api/dashboard', requireAuth, (req, res) => {
// Post counts by status
const postsByStatus = db.prepare('SELECT status, COUNT(*) as count FROM posts GROUP BY status').all();
const totalPosts = db.prepare('SELECT COUNT(*) as count FROM posts').get().count;
const isSuperadmin = req.session.userRole === 'superadmin';
const userId = req.session.userId;
// Active campaigns
const activeCampaigns = db.prepare("SELECT COUNT(*) as count FROM campaigns WHERE status = 'active'").get().count;
const totalCampaigns = db.prepare('SELECT COUNT(*) as count FROM campaigns').get().count;
// Subquery for user's campaign IDs (non-superadmins)
const myCampaignIds = `SELECT id FROM campaigns WHERE created_by_user_id = ${userId} UNION SELECT campaign_id FROM campaign_assignments WHERE user_id = ${userId}`;
const campaignFilter = isSuperadmin
? ''
: `AND (c.created_by_user_id = ${userId} OR c.id IN (SELECT campaign_id FROM campaign_assignments WHERE user_id = ${userId}))`;
// Overdue tasks
// Post counts by status (scoped to user's campaigns for non-superadmins)
const postCampaignFilter = isSuperadmin ? '' : `WHERE p.campaign_id IN (${myCampaignIds})`;
const postsByStatus = db.prepare(`SELECT status, COUNT(*) as count FROM posts p ${postCampaignFilter} GROUP BY status`).all();
const totalPosts = db.prepare(`SELECT COUNT(*) as count FROM posts p ${postCampaignFilter}`).get().count;
// Active campaigns (filtered by assignment for non-superadmins)
const activeCampaigns = db.prepare(`SELECT COUNT(*) as count FROM campaigns c WHERE status = 'active' ${campaignFilter}`).get().count;
const totalCampaigns = db.prepare(`SELECT COUNT(*) as count FROM campaigns c WHERE 1=1 ${campaignFilter}`).get().count;
// Overdue tasks (scoped to user's campaigns via projects for non-superadmins)
const taskCampaignFilter = isSuperadmin ? '' : `AND (tasks.created_by_user_id = ${userId} OR tasks.project_id IN (SELECT id FROM projects WHERE created_by_user_id = ${userId}))`;
const overdueTasks = db.prepare(`
SELECT COUNT(*) as count FROM tasks
WHERE due_date < date('now') AND status != 'done'
SELECT COUNT(*) as count FROM tasks
WHERE due_date < date('now') AND status != 'done' ${taskCampaignFilter}
`).get().count;
// Total tasks by status
const tasksByStatus = db.prepare('SELECT status, COUNT(*) as count FROM tasks GROUP BY status').all();
// Total tasks by status (scoped)
const tasksByStatus = db.prepare(`SELECT status, COUNT(*) as count FROM tasks WHERE 1=1 ${taskCampaignFilter} GROUP BY status`).all();
// Team workload (tasks assigned per member)
const teamWorkload = db.prepare(`
// Team workload — only for superadmins
const teamWorkload = isSuperadmin ? db.prepare(`
SELECT t.id, t.name, t.role,
COUNT(CASE WHEN tk.status != 'done' THEN 1 END) as active_tasks,
COUNT(CASE WHEN tk.status = 'done' THEN 1 END) as completed_tasks,
@@ -1331,26 +1501,29 @@ app.get('/api/dashboard', requireAuth, (req, res) => {
LEFT JOIN posts p ON p.assigned_to = t.id
GROUP BY t.id
ORDER BY active_tasks DESC
`).all();
`).all() : [];
// Active projects
const activeProjects = db.prepare("SELECT COUNT(*) as count FROM projects WHERE status = 'active'").get().count;
// Active projects (scoped for non-superadmins)
const projectFilter = isSuperadmin ? '' : `AND (created_by_user_id = ${userId})`;
const activeProjects = db.prepare(`SELECT COUNT(*) as count FROM projects WHERE status = 'active' ${projectFilter}`).get().count;
// Recent posts
// Recent posts (scoped to user's campaigns for non-superadmins)
const recentPostFilter = isSuperadmin ? '' : `WHERE p.campaign_id IN (${myCampaignIds})`;
const recentPosts = db.prepare(`
SELECT p.*, b.name as brand_name, t.name as assigned_name
FROM posts p
LEFT JOIN brands b ON p.brand_id = b.id
FROM posts p
LEFT JOIN brands b ON p.brand_id = b.id
LEFT JOIN team_members t ON p.assigned_to = t.id
${recentPostFilter}
ORDER BY p.updated_at DESC LIMIT 5
`).all();
// Upcoming campaigns
// Upcoming campaigns (filtered by assignment for non-superadmins)
const upcomingCampaigns = db.prepare(`
SELECT c.*, b.name as brand_name
FROM campaigns c
SELECT c.*, b.name as brand_name
FROM campaigns c
LEFT JOIN brands b ON c.brand_id = b.id
WHERE c.end_date >= date('now')
WHERE c.end_date >= date('now') ${campaignFilter}
ORDER BY c.start_date ASC LIMIT 5
`).all();
@@ -1417,6 +1590,30 @@ app.post('/api/comments/:entityType/:entityId', requireAuth, (req, res) => {
res.status(201).json(comment);
});
app.patch('/api/comments/:id', requireAuth, (req, res) => {
const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(req.params.id);
if (!comment) return res.status(404).json({ error: 'Comment not found' });
// Only the comment author can edit
if (comment.user_id !== req.session.userId) {
return res.status(403).json({ error: 'You can only edit your own comments' });
}
const { content } = req.body;
if (!content || !content.trim()) {
return res.status(400).json({ error: 'Content is required' });
}
db.prepare('UPDATE comments SET content = ? WHERE id = ?').run(content.trim(), req.params.id);
const updated = db.prepare(`
SELECT c.*, u.name as user_name, u.avatar as user_avatar
FROM comments c
LEFT JOIN users u ON c.user_id = u.id
WHERE c.id = ?
`).get(req.params.id);
res.json(updated);
});
app.delete('/api/comments/:id', requireAuth, (req, res) => {
const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(req.params.id);
if (!comment) return res.status(404).json({ error: 'Comment not found' });