import { Elysia, t } from "elysia"; import sql from "../db"; import { createRateLimiter } from "../../../shared/rate-limit"; import { getPlanLimits } from "../../../shared/plans"; import { generateKey, hashEmail, resolveKey as sharedResolveKey, extractAuthKey, COOKIE_OPTS } from "../../../shared/auth"; const checkAuthRateLimit = createRateLimiter(); async function resolveKey(key: string) { return sharedResolveKey(sql, key); } export { resolveKey }; export function requireAuth(app: Elysia) { return app .derive(async ({ headers, cookie, set }) => { const key = extractAuthKey(headers, cookie); if (!key) { set.status = 401; return { accountId: null as string | null, keyId: null as string | null, plan: "free" as string }; } const resolved = await resolveKey(key); if (resolved) return { accountId: resolved.accountId, keyId: resolved.keyId, plan: resolved.plan }; set.status = 401; return { accountId: null as string | null, keyId: null as string | null, plan: "free" as string }; }) .onBeforeHandle(({ accountId, set }) => { if (!accountId) { set.status = 401; return { error: "Invalid or missing account key" }; } }); } export const account = new Elysia({ prefix: "/account" }) .post("/login", async ({ body, cookie, set, request }) => { const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; if (!checkAuthRateLimit(ip, 10)) { set.status = 429; return { error: "Too many login attempts. Try again later." }; } const key = (body.key as string)?.trim(); if (!key) { set.status = 400; return { error: "Key required" }; } const resolved = await resolveKey(key); if (!resolved) { set.status = 401; if ((body as any)._form) { set.redirect = "/dashboard?error=invalid"; return; } return { error: "Invalid account key" }; } cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); if ((body as any)._form) { set.redirect = "/dashboard/home"; return; } return { ok: true }; }, { detail: { hide: true } }) .get("/logout", ({ cookie, set }) => { cookie.pingql_key.set({ value: "", ...COOKIE_OPTS, maxAge: 0 }); set.redirect = "/dashboard"; }, { detail: { hide: true } }) .post("/register", async ({ body, cookie, request, set }) => { const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; if (!checkAuthRateLimit(ip, 5)) { set.status = 429; return { error: "Too many registrations. Try again later." }; } const key = generateKey(); const emailHash = body.email ? hashEmail(body.email) : null; await sql`INSERT INTO accounts (key, email_hash) VALUES (${key}, ${emailHash})`; cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); 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." })), }), }) .use(requireAuth) .get("/settings", async ({ accountId }) => { const [acc] = await sql`SELECT id, email_hash, plan, plan_expires_at, plan_stack, created_at FROM accounts WHERE id = ${accountId}`; const keys = await sql`SELECT id, key, label, created_at, last_used_at FROM api_keys WHERE account_id = ${accountId} ORDER BY created_at DESC`; const [{ count: monitorCount }] = await sql`SELECT COUNT(*)::int as count FROM monitors WHERE account_id = ${accountId}`; const limits = getPlanLimits(acc.plan); return { account_id: acc.id, has_email: !!acc.email_hash, plan: acc.plan, plan_expires_at: acc.plan_expires_at, plan_stack: acc.plan_stack || [], monitor_count: monitorCount, limits, created_at: acc.created_at, api_keys: keys, }; }) .post("/email", async ({ accountId, keyId, body, set }) => { if (keyId) { set.status = 403; return { error: "Sub-keys cannot modify account email" }; } 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." }))), }), }) .post("/reset-key", async ({ accountId, keyId, cookie, set }) => { if (keyId) { set.status = 403; return { error: "Sub-keys cannot rotate the account key" }; } const key = generateKey(); await sql`UPDATE accounts SET key = ${key} WHERE id = ${accountId}`; cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); return { key, message: "Primary key rotated. Your old key is now invalid." }; }) .post("/keys", async ({ accountId, keyId, body, set }) => { if (keyId) { set.status = 403; return { error: "Sub-keys cannot create other sub-keys" }; } const key = generateKey(); const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${body.label}) RETURNING id`; return { key, id: created.id, label: body.label }; }, { body: t.Object({ label: t.String({ description: "A name for this key, e.g. 'ci-pipeline' or 'mobile-app'" }), }), }) .delete("/keys/:id", async ({ accountId, keyId, params, set }) => { if (keyId) { set.status = 403; return { error: "Sub-keys cannot revoke other sub-keys" }; } const [deleted] = await sql` DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId} RETURNING id `; if (!deleted) { set.status = 404; return { error: "Key not found" }; } return { deleted: true }; });