import { Router } from "@oak/oak"; import { query, queryOne, execute } from "../db/postgres.ts"; import { AppError } from "../middleware/error.ts"; import { authMiddleware, requireChef } from "../middleware/auth.ts"; import type { Module, OrganizationModule } from "../types/index.ts"; export const modulesRouter = new Router({ prefix: "/api/modules" }); // Get all available modules modulesRouter.get("/", authMiddleware, async (ctx) => { const modules = await query( `SELECT * FROM modules ORDER BY is_core DESC, name` ); ctx.response.body = { modules }; }); // Get organization's module configuration modulesRouter.get("/org", authMiddleware, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const modules = await query }>( `SELECT m.*, COALESCE(om.enabled, false) as enabled, COALESCE(om.config, m.default_config) as config FROM modules m LEFT JOIN organization_modules om ON m.id = om.module_id AND om.org_id = $1 ORDER BY m.is_core DESC, m.name`, [orgId] ); ctx.response.body = { modules }; }); // Enable/disable module for organization (Chef only) modulesRouter.post("/:moduleId/toggle", requireChef, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const moduleId = ctx.params.moduleId; const body = await ctx.request.body.json(); const { enabled } = body; if (enabled === undefined) { throw new AppError("enabled field required", 400); } // Get module const module = await queryOne( `SELECT * FROM modules WHERE id = $1`, [moduleId] ); if (!module) { throw new AppError("Module not found", 404); } // Cannot disable core modules if (module.is_core && !enabled) { throw new AppError("Cannot disable core module", 400); } // Upsert organization_modules await execute( `INSERT INTO organization_modules (org_id, module_id, enabled, config) VALUES ($1, $2, $3, $4) ON CONFLICT (org_id, module_id) DO UPDATE SET enabled = $3, updated_at = NOW()`, [orgId, moduleId, enabled, module.default_config] ); ctx.response.body = { message: `Module ${enabled ? "enabled" : "disabled"}`, module: module.name, enabled }; }); // Update module configuration (Chef only) modulesRouter.put("/:moduleId/config", requireChef, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const moduleId = ctx.params.moduleId; const body = await ctx.request.body.json(); const { config } = body; if (!config || typeof config !== "object") { throw new AppError("config object required", 400); } // Verify module exists const module = await queryOne( `SELECT * FROM modules WHERE id = $1`, [moduleId] ); if (!module) { throw new AppError("Module not found", 404); } // Upsert with new config await execute( `INSERT INTO organization_modules (org_id, module_id, enabled, config) VALUES ($1, $2, true, $3) ON CONFLICT (org_id, module_id) DO UPDATE SET config = $3, updated_at = NOW()`, [orgId, moduleId, JSON.stringify(config)] ); ctx.response.body = { message: "Module configuration updated" }; }); // Check if specific module is enabled for current org modulesRouter.get("/check/:moduleName", authMiddleware, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const moduleName = ctx.params.moduleName; const result = await queryOne<{ enabled: boolean; config: Record }>( `SELECT om.enabled, COALESCE(om.config, m.default_config) as config FROM modules m LEFT JOIN organization_modules om ON m.id = om.module_id AND om.org_id = $2 WHERE m.name = $1`, [moduleName, orgId] ); if (!result) { throw new AppError("Module not found", 404); } ctx.response.body = { module: moduleName, enabled: result.enabled ?? false, config: result.config }; }); // ============ DEVELOPER PANEL ENDPOINTS ============ // These require the 'developer' module to be enabled and special permissions // Get system status modulesRouter.get("/developer/status", requireChef, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; // Check if developer module is enabled const devModule = await queryOne<{ enabled: boolean }>( `SELECT om.enabled FROM organization_modules om JOIN modules m ON om.module_id = m.id WHERE m.name = 'developer' AND om.org_id = $1`, [orgId] ); if (!devModule?.enabled) { throw new AppError("Developer module not enabled", 403); } // Get stats const stats = await queryOne<{ user_count: number; order_count: number; timesheet_count: number; enabled_modules: number; }>( `SELECT (SELECT COUNT(*) FROM users WHERE org_id = $1) as user_count, (SELECT COUNT(*) FROM orders WHERE org_id = $1) as order_count, (SELECT COUNT(*) FROM timesheets t JOIN users u ON t.user_id = u.id WHERE u.org_id = $1) as timesheet_count, (SELECT COUNT(*) FROM organization_modules WHERE org_id = $1 AND enabled = true) as enabled_modules`, [orgId] ); ctx.response.body = { status: "ok", organization: orgId, stats, serverTime: new Date().toISOString(), }; }); // Get audit logs modulesRouter.get("/developer/logs", requireChef, async (ctx) => { const { org_id: orgId } = ctx.state.auth.user; const limit = parseInt(ctx.request.url.searchParams.get("limit") || "50"); const offset = parseInt(ctx.request.url.searchParams.get("offset") || "0"); // Check developer module const devModule = await queryOne<{ enabled: boolean }>( `SELECT om.enabled FROM organization_modules om JOIN modules m ON om.module_id = m.id WHERE m.name = 'developer' AND om.org_id = $1`, [orgId] ); if (!devModule?.enabled) { throw new AppError("Developer module not enabled", 403); } const logs = await query<{ id: string; action: string; entity_type: string; user_email: string; created_at: Date; }>( `SELECT al.id, al.action, al.entity_type, al.created_at, u.email as user_email FROM audit_logs al LEFT JOIN users u ON al.user_id = u.id WHERE al.org_id = $1 ORDER BY al.created_at DESC LIMIT $2 OFFSET $3`, [orgId, limit, offset] ); ctx.response.body = { logs }; });