Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
f8cd37ac8c
1823 changed files with 145167 additions and 0 deletions
304
api/src/routes/admin.ts
Normal file
304
api/src/routes/admin.ts
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue