const { query } = require('../utils/db'); const { auditLog } = require('../utils/auditLogger'); const { v4: uuidv4 } = require('uuid'); const crypto = require('crypto'); /** * GET /api/admin/users * List all users in the admin's organization */ const listUsers = async (req, res) => { try { const { organization_id: orgId } = req.user; const { rows } = await query( `SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE organization_id = $1 ORDER BY created_at ASC`, [orgId] ); return res.json(rows); } catch (err) { console.error('List users error:', err); return res.status(500).json({ error: 'Failed to retrieve users.' }); } }; /** * PATCH /api/admin/users/:id/role * Change a user's role */ const updateUserRole = async (req, res) => { try { const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user; const { id } = req.params; const { role } = req.body; if (!['admin', 'member'].includes(role)) { return res.status(400).json({ error: 'Role must be admin or member.' }); } // Cannot change own role if (id === actorId) { return res.status(400).json({ error: 'You cannot change your own role.' }); } const { rows } = await query( `UPDATE users SET role = $1, updated_at = NOW() WHERE id = $2 AND organization_id = $3 RETURNING id, name, email, role, is_active`, [role, id, orgId] ); if (!rows.length) { return res.status(404).json({ error: 'User not found in your organization.' }); } await auditLog({ organizationId: orgId, actorId, actorName, actorEmail, action: 'USER_ROLE_CHANGED', entityType: 'user', newValues: { userId: id, newRole: role }, }); return res.json(rows[0]); } catch (err) { console.error('Update role error:', err); return res.status(500).json({ error: 'Failed to update role.' }); } }; /** * PATCH /api/admin/users/:id/deactivate */ const deactivateUser = async (req, res) => { try { const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user; const { id } = req.params; if (id === actorId) { return res.status(400).json({ error: 'You cannot deactivate your own account.' }); } const { rows } = await query( `UPDATE users SET is_active = false, updated_at = NOW() WHERE id = $1 AND organization_id = $2 RETURNING id, name, email, role, is_active`, [id, orgId] ); if (!rows.length) { return res.status(404).json({ error: 'User not found in your organization.' }); } await auditLog({ organizationId: orgId, actorId, actorName, actorEmail, action: 'USER_DEACTIVATED', entityType: 'user', newValues: { userId: id }, }); return res.json(rows[0]); } catch (err) { console.error('Deactivate user error:', err); return res.status(500).json({ error: 'Failed to deactivate user.' }); } }; /** * POST /api/admin/invites * Send an invite to join the organization */ const createInvite = async (req, res) => { try { const { organization_id: orgId, id: actorId, name: actorName, email: actorEmail } = req.user; const { email, role = 'member' } = req.body; if (!['admin', 'member'].includes(role)) { return res.status(400).json({ error: 'Role must be admin or member.' }); } // Check if email already a member const existing = await query(`SELECT id FROM users WHERE email = $1`, [email.toLowerCase()]); if (existing.rows.length) { return res.status(409).json({ error: 'A user with this email already exists.' }); } const token = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days const { rows } = await query( `INSERT INTO invites (id, organization_id, email, role, token, invited_by, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, email, role, token, expires_at`, [uuidv4(), orgId, email.toLowerCase(), role, token, actorId, expiresAt] ); await auditLog({ organizationId: orgId, actorId, actorName, actorEmail, action: 'USER_INVITED', entityType: 'invite', newValues: { invitedEmail: email, role }, }); return res.status(201).json({ ...rows[0], inviteUrl: `${process.env.FRONTEND_URL}/accept-invite?token=${token}`, }); } catch (err) { console.error('Create invite error:', err); return res.status(500).json({ error: 'Failed to create invite.' }); } }; /** * GET /api/admin/invites */ const listInvites = async (req, res) => { try { const { organization_id: orgId } = req.user; const { rows } = await query( `SELECT i.id, i.email, i.role, i.expires_at, i.used_at, i.created_at, u.name AS invited_by_name FROM invites i LEFT JOIN users u ON u.id = i.invited_by WHERE i.organization_id = $1 ORDER BY i.created_at DESC`, [orgId] ); return res.json(rows); } catch (err) { console.error('List invites error:', err); return res.status(500).json({ error: 'Failed to retrieve invites.' }); } }; /** * GET /api/admin/audit-logs */ const getAuditLogs = async (req, res) => { try { const { organization_id: orgId } = req.user; const { page = 1, limit = 50, action, task_id } = req.query; const pageNum = Math.max(1, parseInt(page)); const limitNum = Math.min(200, Math.max(1, parseInt(limit))); const offset = (pageNum - 1) * limitNum; const conditions = [`organization_id = $1`]; const params = [orgId]; let idx = 2; if (action) { conditions.push(`action = $${idx++}`); params.push(action); } if (task_id) { conditions.push(`task_id = $${idx++}`); params.push(task_id); } const where = `WHERE ${conditions.join(' AND ')}`; const countResult = await query(`SELECT COUNT(*) FROM audit_logs ${where}`, params); const total = parseInt(countResult.rows[0].count); const { rows } = await query( `SELECT id, task_id, actor_id, actor_name, actor_email, action, entity_type, old_values, new_values, metadata, created_at FROM audit_logs ${where} ORDER BY created_at DESC LIMIT $${idx} OFFSET $${idx + 1}`, [...params, limitNum, offset] ); return res.json({ logs: rows, pagination: { page: pageNum, limit: limitNum, total, totalPages: Math.ceil(total / limitNum) }, }); } catch (err) { console.error('Audit logs error:', err); return res.status(500).json({ error: 'Failed to retrieve audit logs.' }); } }; /** * GET /api/admin/org * Get organization info */ const getOrg = async (req, res) => { try { const { organization_id: orgId } = req.user; const { rows } = await query(`SELECT id, name, slug, created_at FROM organizations WHERE id = $1`, [orgId]); return res.json(rows[0]); } catch (err) { return res.status(500).json({ error: 'Failed to retrieve organization.' }); } }; /** * GET /api/users - org-scoped user list for dropdowns (any authenticated user) */ const listOrgUsers = async (req, res) => { try { const { organization_id: orgId } = req.user; const { rows } = await query( `SELECT id, name, email, role FROM users WHERE organization_id = $1 AND is_active = true ORDER BY name`, [orgId] ); return res.json(rows); } catch (err) { return res.status(500).json({ error: 'Failed to retrieve users.' }); } }; module.exports = { listUsers, updateUserRole, deactivateUser, createInvite, listInvites, getAuditLogs, getOrg, listOrgUsers };