import { Hono } from "jsr:@hono/hono@4"; import { encodeBase64Url } from "jsr:@std/encoding@1/base64url"; import { sqlWrapper } from "../../db.ts"; import { auditWrapper } from "../../audit.ts"; import { getAuthenticatedUser } from "../../auth-session.ts"; import { getClientIp } from "../../middleware.ts"; export const invitesAdminRoutes = new Hono(); invitesAdminRoutes.get("/", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const invites = await sqlWrapper.sql` SELECT i.id, i.code, i.role, i.max_uses, i.uses_count, i.auto_activate, i.expires_at, i.created_at, i.used_at, a.name AS app_name, a.id AS app_id, u.username AS used_by_username FROM invites i LEFT JOIN apps a ON i.app_id = a.id LEFT JOIN users u ON i.used_by = u.id ORDER BY i.created_at DESC `; return c.json({ invites }); }); invitesAdminRoutes.post("/create", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const { appId, role, expiresInDays, customCode, usageLimitType, maxUses, autoActivate, } = await c.req.json(); const assignedRole = (typeof role === "string" && role.trim()) ? role.trim() : "user"; let validatedAppId = null; if (appId) { const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; if (typeof appId !== "string" || !uuidRegex.test(appId)) { return c.json({ error: "appId must be a valid UUID string" }, 400); } const appExists = await sqlWrapper .sql`SELECT id FROM apps WHERE id = ${appId}`.then( (res: any) => res[0], ); if (!appExists) { return c.json({ error: "Target application does not exist" }, 404); } validatedAppId = appId; } let parsedMaxUses: number | null = 1; if (usageLimitType === "unlimited") { parsedMaxUses = null; } else if (usageLimitType === "limited") { const n = parseInt(maxUses); parsedMaxUses = (!isNaN(n) && n > 0) ? n : 5; } else { parsedMaxUses = 1; // single-use default } const shouldAutoActivate = autoActivate !== false; const inviteCode = (customCode && typeof customCode === "string" && customCode.trim()) ? customCode.trim() : encodeBase64Url(crypto.getRandomValues(new Uint8Array(24))); const days = Number(expiresInDays) || 7; if (!Number.isInteger(days) || days < 1 || days > 30) { return c.json({ error: "expiresInDays must be an integer between 1 and 30", }, 400); } const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + days); try { await sqlWrapper.sql` INSERT INTO invites (code, app_id, role, created_by, max_uses, uses_count, auto_activate, expires_at) VALUES (${inviteCode}, ${validatedAppId}, ${assignedRole}, ${auth.userId}, ${parsedMaxUses}, 0, ${shouldAutoActivate}, ${expiresAt}) `; } catch (err: any) { if (err.code === "23505") { return c.json({ error: "An invite with this code already exists" }, 409); } return c.json({ error: "Failed to generate invite" }, 500); } auditWrapper.auditLog( auth.userId, "invite_created", validatedAppId, { code: inviteCode, role: assignedRole, max_uses: parsedMaxUses, auto_activate: shouldAutoActivate, expiresInDays: days, }, getClientIp(c), ); return c.json({ success: true, inviteCode, expiresAt, maxUses: parsedMaxUses, autoActivate: shouldAutoActivate, }); }); invitesAdminRoutes.get("/:id/redemptions", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const inviteId = c.req.param("id"); const redemptions = await sqlWrapper.sql` SELECT ir.id, ir.redeemed_at, u.id AS user_id, u.username, u.display_name, u.account_status FROM invite_redemptions ir JOIN users u ON ir.user_id = u.id WHERE ir.invite_id = ${inviteId} ORDER BY ir.redeemed_at DESC `; return c.json({ redemptions }); }); invitesAdminRoutes.delete("/:id", async (c) => { const auth = await getAuthenticatedUser(c); if (!auth) return c.json({ error: "Unauthorized" }, 401); const inviteId = c.req.param("id"); const invite = await sqlWrapper .sql`DELETE FROM invites WHERE id = ${inviteId} RETURNING id, code` .then((res: any) => res[0]); if (invite) { auditWrapper.auditLog( auth.userId, "invite_revoked", inviteId, { code: invite.code }, getClientIp(c), ); return c.json({ success: true }); } return c.json({ error: "Invite not found" }, 404); });