import { Router } from "express"; import bcrypt from "bcryptjs"; import { pool } from "../db.js"; import { requireAdmin, signAdminToken } from "../middleware/admin-auth.js"; import { submitLimiter } from "../middleware/rate-limit.js"; const router = Router(); // ===================================================================== // Auth // ===================================================================== /** POST /api/v1/admin/login — Authenticate and receive JWT. */ router.post("/api/v1/admin/login", submitLimiter, async (req, res) => { try { const { username, password } = req.body ?? {}; if (!username || !password) { res.status(400).json({ error: "Username and password required." }); return; } const result = await pool.query( "SELECT id, username, password_hash, display_name, active FROM admin_users WHERE username = $1", [username.toLowerCase().trim()], ); if (result.rows.length === 0) { res.status(401).json({ error: "Invalid credentials." }); return; } const user = result.rows[0]; if (!user.active) { res.status(403).json({ error: "Account is disabled." }); return; } const valid = await bcrypt.compare(password, user.password_hash); if (!valid) { res.status(401).json({ error: "Invalid credentials." }); return; } // Update last login await pool.query("UPDATE admin_users SET last_login_at = now() WHERE id = $1", [user.id]); const token = signAdminToken({ id: user.id, username: user.username }); res.json({ token, user: { id: user.id, username: user.username, display_name: user.display_name }, }); } catch (err) { console.error("[admin/login] Error:", err); res.status(500).json({ error: "Login failed." }); } }); /** GET /api/v1/admin/me — Verify token and return current user. */ router.get("/api/v1/admin/me", requireAdmin, async (req, res) => { res.json({ user: req.admin }); }); // ===================================================================== // Order Queue — Formation Orders // ===================================================================== /** GET /api/v1/admin/formations — List all formation orders with filtering. */ router.get("/api/v1/admin/formations", requireAdmin, async (req, res) => { try { const status = req.query.status as string || ""; const automation = req.query.automation as string || ""; const priority = req.query.priority as string || ""; const assigned = req.query.assigned as string || ""; const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 200); const offset = parseInt(req.query.offset as string, 10) || 0; let where = "WHERE 1=1"; const params: any[] = []; let paramIdx = 1; if (status) { where += ` AND f.status = $${paramIdx++}`; params.push(status); } if (automation) { where += ` AND f.automation_status = $${paramIdx++}`; params.push(automation); } if (priority) { where += ` AND f.priority = $${paramIdx++}`; params.push(priority); } if (assigned === "unassigned") { where += " AND f.assigned_to IS NULL"; } else if (assigned === "me") { where += ` AND f.assigned_to = $${paramIdx++}`; params.push(req.admin!.id); } else if (assigned) { where += ` AND f.assigned_to = $${paramIdx++}`; params.push(parseInt(assigned, 10)); } const countResult = await pool.query( `SELECT COUNT(*) as total FROM formation_orders f ${where}`, params, ); params.push(limit, offset); const result = await pool.query( `SELECT f.*, a.username as assigned_username, a.display_name as assigned_name FROM formation_orders f LEFT JOIN admin_users a ON f.assigned_to = a.id ${where} ORDER BY CASE f.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END, f.created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`, params, ); res.json({ orders: result.rows, total: parseInt(countResult.rows[0].total, 10), limit, offset, }); } catch (err) { console.error("[admin/formations] Error:", err); res.status(500).json({ error: "Could not load orders." }); } }); /** GET /api/v1/admin/formations/:id — Single order with full details + audit log. */ router.get("/api/v1/admin/formations/:id", requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); const order = await pool.query( `SELECT f.*, a.username as assigned_username, a.display_name as assigned_name FROM formation_orders f LEFT JOIN admin_users a ON f.assigned_to = a.id WHERE f.id = $1`, [id], ); if (order.rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; } const audit = await pool.query( `SELECT * FROM order_audit_log WHERE order_type = 'formation' AND order_id = $1 ORDER BY created_at DESC`, [id], ); const discount = await pool.query( `SELECT * FROM discount_usage WHERE order_type = 'formation' AND order_id = $1`, [id], ); res.json({ order: order.rows[0], audit_log: audit.rows, discount: discount.rows[0] || null, }); } catch (err) { console.error("[admin/formations/:id] Error:", err); res.status(500).json({ error: "Could not load order." }); } }); /** PATCH /api/v1/admin/formations/:id — Update order status, priority, assignment, notes. */ router.patch("/api/v1/admin/formations/:id", requireAdmin, async (req, res) => { try { const id = parseInt(req.params.id, 10); const { status, automation_status, priority, assigned_to, admin_notes, note } = req.body ?? {}; // Fetch current state const current = await pool.query("SELECT * FROM formation_orders WHERE id = $1", [id]); if (current.rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; } const order = current.rows[0]; const updates: string[] = []; const params: any[] = []; let idx = 1; if (status && status !== order.status) { updates.push(`status = $${idx++}`); params.push(status); // Log status change await pool.query( `INSERT INTO order_audit_log (order_type, order_id, order_number, action, from_status, to_status, actor_type, actor_id, actor_name, note) VALUES ('formation', $1, $2, 'status_change', $3, $4, 'admin', $5, $6, $7)`, [id, order.order_number, order.status, status, req.admin!.id, req.admin!.username, note || null], ); if (status === "delivered") { updates.push(`delivered_at = now()`); } if (status === "filed") { updates.push(`filed_at = now()`); } } if (automation_status && automation_status !== order.automation_status) { updates.push(`automation_status = $${idx++}`); params.push(automation_status); await pool.query( `INSERT INTO order_audit_log (order_type, order_id, order_number, action, from_status, to_status, actor_type, actor_id, actor_name, note) VALUES ('formation', $1, $2, 'automation_update', $3, $4, 'admin', $5, $6, $7)`, [id, order.order_number, order.automation_status, automation_status, req.admin!.id, req.admin!.username, note || null], ); } if (priority && priority !== order.priority) { updates.push(`priority = $${idx++}`); params.push(priority); } if (assigned_to !== undefined) { updates.push(`assigned_to = $${idx++}`); params.push(assigned_to || null); await pool.query( `INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note) VALUES ('formation', $1, $2, 'assigned', 'admin', $3, $4, $5)`, [id, order.order_number, req.admin!.id, req.admin!.username, `Assigned to admin #${assigned_to || "unassigned"}`], ); } if (admin_notes !== undefined) { updates.push(`admin_notes = $${idx++}`); params.push(admin_notes); } // Add a note to audit log if provided without other changes if (note && !status && !automation_status) { await pool.query( `INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note) VALUES ('formation', $1, $2, 'note_added', 'admin', $3, $4, $5)`, [id, order.order_number, req.admin!.id, req.admin!.username, note], ); } if (updates.length > 0) { updates.push("last_activity_at = now()"); updates.push(`updated_at = now()`); params.push(id); await pool.query( `UPDATE formation_orders SET ${updates.join(", ")} WHERE id = $${idx}`, params, ); } res.json({ success: true, message: "Order updated." }); } catch (err) { console.error("[admin/formations/:id PATCH] Error:", err); res.status(500).json({ error: "Could not update order." }); } }); // ===================================================================== // Dashboard Stats // ===================================================================== /** GET /api/v1/admin/stats — Queue overview counts. */ router.get("/api/v1/admin/stats", requireAdmin, async (req, res) => { try { const formations = await pool.query(` SELECT COUNT(*) FILTER (WHERE status = 'received') as received, COUNT(*) FILTER (WHERE status = 'processing') as processing, COUNT(*) FILTER (WHERE status = 'submitted') as submitted, COUNT(*) FILTER (WHERE status = 'filed') as filed, COUNT(*) FILTER (WHERE status = 'delivered') as delivered, COUNT(*) FILTER (WHERE status = 'cancelled') as cancelled, COUNT(*) FILTER (WHERE automation_status = 'failed') as automation_failed, COUNT(*) FILTER (WHERE automation_status = 'manual') as manual_required, COUNT(*) FILTER (WHERE priority = 'urgent') as urgent, COUNT(*) FILTER (WHERE assigned_to IS NULL AND status NOT IN ('delivered','cancelled')) as unassigned, COUNT(*) as total FROM formation_orders `); const subscribers = await pool.query( "SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE unsubscribed = FALSE) as active FROM subscribers", ); const quotes = await pool.query( "SELECT COUNT(*) FILTER (WHERE status = 'pending') as pending FROM quotes", ); const tickets = await pool.query( "SELECT COUNT(*) as total FROM tickets WHERE created_at > now() - interval '24 hours'", ); const revenue = await pool.query( "SELECT COALESCE(SUM(total_cents), 0) as total_cents FROM formation_orders WHERE status NOT IN ('cancelled')", ); res.json({ formations: formations.rows[0], subscribers: subscribers.rows[0], quotes: quotes.rows[0], tickets_24h: parseInt(tickets.rows[0].total, 10), revenue_cents: parseInt(revenue.rows[0].total_cents, 10), }); } catch (err) { console.error("[admin/stats] Error:", err); res.status(500).json({ error: "Could not load stats." }); } }); // ===================================================================== // Audit Log // ===================================================================== /** GET /api/v1/admin/audit — Recent audit log entries. */ router.get("/api/v1/admin/audit", requireAdmin, async (req, res) => { try { const limit = Math.min(parseInt(req.query.limit as string, 10) || 50, 200); const result = await pool.query( "SELECT * FROM order_audit_log ORDER BY created_at DESC LIMIT $1", [limit], ); res.json({ entries: result.rows }); } catch (err) { console.error("[admin/audit] Error:", err); res.status(500).json({ error: "Could not load audit log." }); } }); export default router;