import { Elysia, t } from "elysia"; import { randomBytes, createHash } from "crypto"; import sql from "../db"; function generateKey(): string { const bytes = randomBytes(8); const hex = bytes.toString("hex").toUpperCase(); return `${hex.slice(0, 4)}-${hex.slice(4, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}`; } function hashEmail(email: string): string { return createHash("sha256").update(email.toLowerCase().trim()).digest("hex"); } export function requireAuth(app: Elysia) { return app .derive(async ({ headers, set }) => { const key = headers["authorization"]?.replace("Bearer ", "").trim(); if (!key) { set.status = 401; return { accountId: null as string | null, keyId: null as string | null }; } const [account] = await sql`SELECT id FROM accounts WHERE id = ${key}`; if (account) return { accountId: account.id as string, keyId: null as string | null }; const [apiKey] = await sql`SELECT id, account_id FROM api_keys WHERE id = ${key}`; if (apiKey) { sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${key}`.catch(() => {}); return { accountId: apiKey.account_id as string, keyId: apiKey.id as string }; } set.status = 401; return { accountId: null as string | null, keyId: null as string | null }; }) .onBeforeHandle(({ accountId, set }) => { if (!accountId) { set.status = 401; return { error: "Invalid or missing account key" }; } }); } export const account = new Elysia({ prefix: "/account" }) // ── Register ──────────────────────────────────────────────────────── .post("/register", async ({ body }) => { const key = generateKey(); const emailHash = body.email ? hashEmail(body.email) : null; await sql`INSERT INTO accounts (id, email_hash) VALUES (${key}, ${emailHash})`; return { key, ...(body.email ? { email_registered: true } : { email_registered: false }), }; }, { body: t.Object({ email: t.Optional(t.String({ format: "email", description: "Optional. Used for account recovery only." })), }), }) // ── Auth-required routes below ─────────────────────────────────────── .use(requireAuth) // Get account settings .get("/settings", async ({ accountId }) => { const [acc] = await sql`SELECT id, email_hash, created_at FROM accounts WHERE id = ${accountId}`; const keys = await sql`SELECT id, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; return { account_id: acc.id, has_email: !!acc.email_hash, created_at: acc.created_at, api_keys: keys, }; }) // Update email .post("/email", async ({ accountId, body }) => { const emailHash = body.email ? hashEmail(body.email) : null; await sql`UPDATE accounts SET email_hash = ${emailHash} WHERE id = ${accountId}`; return { ok: true }; }, { body: t.Object({ email: t.Optional(t.Nullable(t.String({ description: "Email for account recovery only." }))), }), }) // Reset primary key — generates a new one, old one immediately invalid .post("/reset-key", async ({ accountId }) => { const newKey = generateKey(); await sql`UPDATE accounts SET id = ${newKey} WHERE id = ${accountId}`; return { key: newKey, message: "Primary key rotated. Your old key is now invalid.", }; }) // Create a sub-key (for different apps or shared access) .post("/keys", async ({ accountId, body }) => { const key = generateKey(); await sql`INSERT INTO api_keys (id, account_id, label) VALUES (${key}, ${accountId}, ${body.label})`; return { key, label: body.label }; }, { body: t.Object({ label: t.String({ description: "A name for this key, e.g. 'ci-pipeline' or 'mobile-app'" }), }), }) // Delete a sub-key .delete("/keys/:id", async ({ accountId, params, error }) => { const [deleted] = await sql` DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id `; if (!deleted) return error(404, { error: "Key not found" }); return { deleted: true }; });