import { Router } from "express"; import bcrypt from "bcryptjs"; import { pool } from "../db.js"; import { requireAdmin, requireAdminQueryOrHeader, 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." }); } }); // ===================================================================== // Compliance Orders (telecom / DOT / healthcare service fulfillment) // // The legacy admin SPA only manages formation_orders. Compliance service // orders (compliance_orders) had no admin surface at all -- you couldn't see // what was paid, what was stuck on intake, or approve a prepared filing for // submission. These endpoints back the /admin/compliance-orders page. // ===================================================================== /** GET /api/v1/admin/compliance-orders — list, grouped by batch, with filters. */ router.get("/api/v1/admin/compliance-orders", requireAdmin, async (req, res) => { try { const payment = (req.query.payment as string) || ""; const fulfillment = (req.query.fulfillment as string) || ""; const intake = (req.query.intake as string) || ""; // 'incomplete' | 'complete' const q = ((req.query.q as string) || "").trim().toLowerCase(); const limit = Math.min(parseInt(req.query.limit as string, 10) || 200, 500); const where: string[] = ["1=1"]; const params: any[] = []; let i = 1; if (payment) { where.push(`co.payment_status = $${i++}`); params.push(payment); } if (fulfillment === "none") { where.push(`co.fulfillment_status IS NULL`); } else if (fulfillment) { where.push(`co.fulfillment_status = $${i++}`); params.push(fulfillment); } if (intake === "incomplete") { where.push(`COALESCE(co.intake_data_validated, FALSE) = FALSE`); } else if (intake === "complete") { where.push(`co.intake_data_validated = TRUE`); } if (q) { where.push(`(lower(co.customer_email) LIKE $${i} OR lower(co.customer_name) LIKE $${i} OR lower(co.order_number) LIKE $${i} OR lower(COALESCE(co.batch_id,'')) LIKE $${i})`); params.push(`%${q}%`); i++; } params.push(limit); const { rows } = await pool.query( `SELECT co.order_number, co.batch_id, co.service_slug, co.service_name, co.customer_email, co.customer_name, co.customer_phone, co.payment_status, co.payment_method, co.paid_at, co.service_fee_cents, co.gov_fee_cents, co.surcharge_cents, co.discount_cents, co.fulfillment_status, co.fulfillment_status_at, COALESCE(co.intake_data_validated, FALSE) AS intake_data_validated, co.intake_reminder_count, co.intake_reminder_last_at, co.erpnext_sales_order, co.created_at FROM compliance_orders co WHERE ${where.join(" AND ")} ORDER BY co.created_at DESC LIMIT $${i}`, params, ); // Group multi-service batches (batch_id) into a single card; standalone // orders (no batch_id) become their own one-item group keyed by order_number. type Grp = { group_id: string; is_batch: boolean; customer_name: string; customer_email: string; customer_phone: string | null; payment_status: string; payment_method: string | null; paid_at: string | null; created_at: string; total_cents: number; intake_all_complete: boolean; intake_any_incomplete: boolean; max_reminder_count: number; last_reminded_at: string | null; services: any[]; }; const groups = new Map(); for (const r of rows as any[]) { const key = r.batch_id || r.order_number; let g = groups.get(key); if (!g) { g = { group_id: key, is_batch: !!r.batch_id, customer_name: r.customer_name, customer_email: r.customer_email, customer_phone: r.customer_phone, payment_status: r.payment_status, payment_method: r.payment_method, paid_at: r.paid_at, created_at: r.created_at, total_cents: 0, intake_all_complete: true, intake_any_incomplete: false, max_reminder_count: 0, last_reminded_at: null, services: [], }; groups.set(key, g); } g.total_cents += Number(r.service_fee_cents || 0) + Number(r.gov_fee_cents || 0) + Number(r.surcharge_cents || 0) - Number(r.discount_cents || 0); if (!r.intake_data_validated) { g.intake_all_complete = false; g.intake_any_incomplete = true; } g.max_reminder_count = Math.max(g.max_reminder_count, Number(r.intake_reminder_count || 0)); if (r.intake_reminder_last_at && (!g.last_reminded_at || r.intake_reminder_last_at > g.last_reminded_at)) { g.last_reminded_at = r.intake_reminder_last_at; } g.services.push({ order_number: r.order_number, service_slug: r.service_slug, service_name: r.service_name || r.service_slug, fulfillment_status: r.fulfillment_status, fulfillment_status_at: r.fulfillment_status_at, intake_data_validated: r.intake_data_validated, erpnext_sales_order: r.erpnext_sales_order, ready_to_approve: r.fulfillment_status === "ready_to_file", }); } res.json({ groups: Array.from(groups.values()) }); } catch (err) { console.error("[admin/compliance-orders] Error:", err); res.status(500).json({ error: "Could not load compliance orders." }); } }); /** GET /api/v1/admin/compliance-orders/stats — queue overview counts. */ router.get("/api/v1/admin/compliance-orders/stats", requireAdmin, async (_req, res) => { try { const { rows } = await pool.query(` SELECT COUNT(*) FILTER (WHERE payment_status = 'paid') AS paid, COUNT(*) FILTER (WHERE payment_status = 'pending_payment') AS pending_payment, COUNT(*) FILTER (WHERE payment_status = 'paid' AND COALESCE(intake_data_validated, FALSE) = FALSE) AS paid_intake_incomplete, COUNT(*) FILTER (WHERE fulfillment_status = 'ready_to_file') AS ready_to_file, COUNT(*) FILTER (WHERE fulfillment_status = 'awaiting_intake') AS awaiting_intake, COUNT(*) FILTER (WHERE fulfillment_status = 'completed') AS completed, COUNT(*) AS total FROM compliance_orders `); res.json(rows[0]); } catch (err) { console.error("[admin/compliance-orders/stats] Error:", err); res.status(500).json({ error: "Could not load stats." }); } }); /** GET /api/v1/admin/compliance-orders/:order_number — single order full detail. */ router.get("/api/v1/admin/compliance-orders/:order_number", requireAdmin, async (req, res) => { try { const { rows } = await pool.query( `SELECT co.*, te.legal_name AS entity_name, te.frn AS entity_frn FROM compliance_orders co LEFT JOIN telecom_entities te ON te.id = co.telecom_entity_id WHERE co.order_number = $1`, [req.params.order_number], ); if (rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; } const order = rows[0]; const audit = await pool.query( `SELECT * FROM order_audit_log WHERE order_number = $1 AND order_type IN ('compliance', 'compliance_batch') ORDER BY created_at DESC`, [order.order_number], ); res.json({ order, audit_log: audit.rows }); } catch (err) { console.error("[admin/compliance-orders/:id] Error:", err); res.status(500).json({ error: "Could not load order." }); } }); /** * POST /api/v1/admin/compliance-orders/:order_number/approve * Approve a prepared filing held at fulfillment_status='ready_to_file' and * dispatch the worker to actually submit it to the government system. */ router.post("/api/v1/admin/compliance-orders/:order_number/approve", requireAdmin, async (req, res) => { const id = req.params.order_number; try { const { rows } = await pool.query( `SELECT order_number, service_slug, fulfillment_status, COALESCE(intake_data_validated, FALSE) AS intake_data_validated FROM compliance_orders WHERE order_number = $1`, [id], ); const order = rows[0]; if (!order) { res.status(404).json({ error: "Order not found." }); return; } if (order.fulfillment_status !== "ready_to_file") { res.status(409).json({ error: `Order is not awaiting submission approval (status=${order.fulfillment_status ?? "none"}).`, }); return; } // Safety gate: never let an order be filed while its intake is still // incomplete (e.g. an admin-assisted service parked at ready_to_file before // the customer finished intake). The caller must pass {force:true} to // override, which is logged in the audit trail. if (!order.intake_data_validated && req.body?.force !== true) { res.status(409).json({ error: "Intake is not complete for this order. Review the documents and resend the intake reminder, or pass force to override.", code: "intake_incomplete", }); return; } await pool.query( `UPDATE compliance_orders SET fulfillment_status = 'authorization_signed', fulfillment_status_at = now(), updated_at = now() WHERE order_number = $1`, [id], ); 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 ('compliance', 0, $1, 'approved_for_submission', 'ready_to_file', 'authorization_signed', 'admin', $2, $3, $4)`, [id, req.admin!.id, req.admin!.username, (req.body?.note as string) || (order.intake_data_validated ? "Approved + dispatched for government submission" : "Approved + dispatched (intake-incomplete override)")], ); const workerUrl = process.env.WORKER_URL || "http://workers:8090"; let dispatched = false; try { const r = await fetch(`${workerUrl}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "process_compliance_service", order_name: id, order_number: id, service_slug: order.service_slug, admin_approved: true, }), }); dispatched = r.ok; } catch (err) { console.error(`[admin/compliance-orders] approve dispatch failed for ${id}:`, err); } res.json({ success: true, order_number: id, dispatched }); } catch (err) { console.error(`[admin/compliance-orders] approve error for ${id}:`, err); res.status(500).json({ error: "Approve failed." }); } }); /** * POST /api/v1/admin/compliance-orders/:order_number/rearm-intake * Re-arm the daily intake reminder for a paid+incomplete order whose reminders * went quiet (clears intake_reminder_last_at so the next daily run nudges it). * Optionally resets the count back to 0 with { reset_count: true }. */ router.post("/api/v1/admin/compliance-orders/:order_number/rearm-intake", requireAdmin, async (req, res) => { const id = req.params.order_number; try { const resetCount = req.body?.reset_count === true; const { rows } = await pool.query( `SELECT order_number, batch_id, payment_status, COALESCE(intake_data_validated, FALSE) AS intake_data_validated FROM compliance_orders WHERE order_number = $1`, [id], ); const order = rows[0]; if (!order) { res.status(404).json({ error: "Order not found." }); return; } if (order.payment_status !== "paid") { res.status(409).json({ error: "Only paid orders can be re-armed." }); return; } if (order.intake_data_validated) { res.status(409).json({ error: "Intake is already complete for this order." }); return; } // Re-arm the whole batch when this order belongs to one, so a multi-service // customer gets a single consolidated nudge (matches the worker grouping). const filter = order.batch_id ? { clause: "batch_id = $1", val: order.batch_id } : { clause: "order_number = $1", val: id }; const updated = await pool.query( `UPDATE compliance_orders SET intake_reminder_last_at = NULL ${resetCount ? ", intake_reminder_count = 0" : ""}, updated_at = now() WHERE ${filter.clause} AND payment_status = 'paid' AND COALESCE(intake_data_validated, FALSE) = FALSE RETURNING order_number`, [filter.val], ); await pool.query( `INSERT INTO order_audit_log (order_type, order_id, order_number, action, actor_type, actor_id, actor_name, note) VALUES ('compliance', 0, $1, 'intake_reminder_rearmed', 'admin', $2, $3, $4)`, [id, req.admin!.id, req.admin!.username, `Re-armed intake reminder for ${updated.rowCount} order(s)${resetCount ? " (count reset to 0)" : ""}`], ); res.json({ success: true, rearmed: updated.rowCount }); } catch (err) { console.error(`[admin/compliance-orders] rearm-intake error for ${id}:`, err); res.status(500).json({ error: "Re-arm failed." }); } }); // ── Document discovery + viewing ───────────────────────────────────────────── // // Every prepared/signed filing PDF lives in MinIO. We surface them so you can // review the actual document (e.g. the signed MCS-150 / authorization) before // approving an order for government submission. Keys come from three places: // 1. esign_records — the unsigned + signed e-sign PDFs per order // 2. intake_data.filing_status — pdf_minio_path / attested_pdf / evidence/* // 3. compliance_orders cols — engagement_letter_minio_key, rmd_packet_minio_paths // // Browsers can't open MinIO's IP-allowlisted public host, so instead of handing // out presigned URLs we STREAM the object through the API (JWT-gated). The // stream endpoint accepts the token via ?token= so it works in a new tab. const WORKER_URL_ADMIN = process.env.WORKER_URL || "http://workers:8090"; // Slugs that actually generate an MCS-150 PDF (mirrors MCS150_FORM_SLUGS in // scripts/workers/services/mcs150_update.py). Other admin-assisted DOT services // routed to the same handler (UCR, MC authority, audit prep, ETA, etc.) never // produce that form, even though the shared filing_status carries a phantom // pdf_minio_path. const FORM_PRODUCING_SLUGS = new Set([ "mcs150-update", "dot-registration", "usdot-reactivation", "dot-full-compliance", ]); /** Ask the worker for a presigned (internal minio:9000) GET URL. */ async function presignInternal(key: string): Promise { if (!key) return null; try { const r = await fetch(`${WORKER_URL_ADMIN}/jobs/presign`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, expires: 600, method: "GET" }), }); if (!r.ok) return null; const data = (await r.json()) as { url?: string }; return data.url || null; } catch { return null; } } /** Collect every known MinIO document key for an order (deduped, labeled). */ async function collectOrderDocuments(orderNumber: string): Promise> { const docs: Array<{ label: string; key: string }> = []; const seen = new Set(); const add = (label: string, key: unknown) => { if (typeof key === "string" && key && !seen.has(key)) { seen.add(key); docs.push({ label, key }); } }; // 1. Order row: intake_data filing artifacts + per-order document columns. const ord = await pool.query( `SELECT service_slug, intake_data, engagement_letter_minio_key, rmd_packet_minio_paths FROM compliance_orders WHERE order_number = $1`, [orderNumber], ); if (ord.rows.length) { const o = ord.rows[0]; const slug = (o.service_slug as string) || ""; const intake = (o.intake_data && typeof o.intake_data === "object" ? o.intake_data : {}) as Record; const fs = (intake.filing_status && typeof intake.filing_status === "object" ? intake.filing_status : {}) as Record; // The shared dot-compliance-remediation pipeline seeds a filing_status with a // template pdf_minio_path on EVERY order in a batch, but only services that // actually produce an MCS-150 form ever generate that PDF. For admin-assisted // services (UCR, MC authority, audit prep, etc.) it's a phantom that never // exists, so only surface the prepared-filing artifacts for form producers. if (FORM_PRODUCING_SLUGS.has(slug)) { add("Prepared filing PDF", fs.pdf_minio_path); add("Attested PDF (faxed)", fs.attested_pdf); } add("Confirmation screenshot", fs.screenshot_path); if (fs.evidence && typeof fs.evidence === "object") { for (const [k, v] of Object.entries(fs.evidence as Record)) { add("Evidence: " + k.replace(/_/g, " "), v); } } add("Engagement letter", o.engagement_letter_minio_key); const rmd = o.rmd_packet_minio_paths; if (Array.isArray(rmd)) rmd.forEach((k, i) => add(`RMD packet ${i + 1}`, k)); else if (rmd && typeof rmd === "object") for (const [k, v] of Object.entries(rmd)) add("RMD: " + k, v); } // 2. e-sign records: the unsigned doc and (once signed) the signed doc. const es = await pool.query( `SELECT document_type, status, document_minio_key, signed_document_minio_key FROM esign_records WHERE order_number = $1 ORDER BY created_at`, [orderNumber], ); for (const r of es.rows as any[]) { const t = (r.document_type || "document").replace(/[-_]/g, " "); if (r.signed_document_minio_key) add(`Signed ${t}`, r.signed_document_minio_key); add(`${r.status === "signed" ? "Unsigned" : "Pending"} ${t}`, r.document_minio_key); } return docs; } /** GET /api/v1/admin/compliance-orders/:order_number/documents — list viewable PDFs. */ router.get("/api/v1/admin/compliance-orders/:order_number/documents", requireAdmin, async (req, res) => { try { const exists = await pool.query("SELECT 1 FROM compliance_orders WHERE order_number = $1", [req.params.order_number]); if (exists.rows.length === 0) { res.status(404).json({ error: "Order not found." }); return; } const docs = await collectOrderDocuments(req.params.order_number); // Verify each object actually exists in storage. A key can be recorded in // the DB before (or without) the object ever being uploaded -- e.g. a // template pdf_minio_path, or a stray esign_records row from the shared // remediation pipeline. We DROP non-existent objects so the UI only ever // lists documents you can actually open; the empty state explains the rest. const checked = await Promise.all(docs.map(async (d) => { let present = false; try { const url = await presignInternal(d.key); if (url) { // The presigned URL is signed for GET, so a HEAD request fails the // signature check. Use a 1-byte ranged GET to confirm existence // cheaply without downloading the whole object. const probe = await fetch(url, { headers: { Range: "bytes=0-0" } }); present = probe.ok; // 200 or 206 } } catch { /* treat as missing */ } return { ...d, exists: present }; })); const available = checked.filter((d) => d.exists); res.json({ order_number: req.params.order_number, documents: available }); } catch (err) { console.error("[admin/compliance-orders/documents] Error:", err); res.status(500).json({ error: "Could not list documents." }); } }); /** * GET /api/v1/admin/compliance-orders/:order_number/document?key=...&token=... * Stream a single MinIO object through the API (JWT-gated via header or ?token). * The key MUST be one we discovered for this order — prevents arbitrary-object * reads from a valid admin token. */ router.get("/api/v1/admin/compliance-orders/:order_number/document", requireAdminQueryOrHeader, async (req, res) => { try { const key = typeof req.query.key === "string" ? req.query.key : ""; if (!key) { res.status(400).json({ error: "key required" }); return; } const docs = await collectOrderDocuments(req.params.order_number); if (!docs.some((d) => d.key === key)) { res.status(403).json({ error: "Key does not belong to this order." }); return; } const url = await presignInternal(key); if (!url) { res.status(502).json({ error: "Could not generate object URL." }); return; } const upstream = await fetch(url); if (upstream.status === 404) { res.status(404).json({ error: "Document not found in storage (it may not have been generated yet)." }); return; } if (!upstream.ok || !upstream.body) { res.status(502).json({ error: `Object fetch failed (${upstream.status}).` }); return; } // Infer a sensible content type; default to PDF (almost all are). const lower = key.toLowerCase(); const ct = lower.endsWith(".pdf") ? "application/pdf" : lower.endsWith(".png") ? "image/png" : lower.endsWith(".jpg") || lower.endsWith(".jpeg") ? "image/jpeg" : lower.endsWith(".xlsx") ? "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : upstream.headers.get("content-type") || "application/octet-stream"; res.setHeader("Content-Type", ct); res.setHeader("Content-Disposition", `inline; filename="${key.split("/").pop() || "document"}"`); const len = upstream.headers.get("content-length"); if (len) res.setHeader("Content-Length", len); const buf = Buffer.from(await upstream.arrayBuffer()); res.end(buf); } catch (err) { console.error("[admin/compliance-orders/document] Error:", err); res.status(500).json({ error: "Could not stream document." }); } }); export default router;