import { Elysia, t } from "elysia"; import { createHmac, randomBytes } from "crypto"; import sql from "../db"; import { createRateLimiter } from "../utils/rate-limit"; const EMAIL_HMAC_KEY = process.env.EMAIL_HMAC_KEY || "pingql-default-hmac-key"; function redir(to: string) { return new Response( ``, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }, ); } // ── Per-IP rate limiting for auth endpoints ─────────────────────────── const checkAuthRateLimit = createRateLimiter(); function generateKey(): string { return randomBytes(32).toString("base64url"); } function hashEmail(email: string): string { return createHmac("sha256", EMAIL_HMAC_KEY).update(email.toLowerCase().trim()).digest("hex"); } async function resolveKey(key: string): Promise<{ accountId: string; keyId: string | null; plan: string } | null> { const [account] = await sql`SELECT id, plan FROM accounts WHERE key = ${key}`; if (account) return { accountId: account.id, keyId: null, plan: account.plan }; const [apiKey] = await sql`SELECT k.id, k.account_id, a.plan FROM api_keys k JOIN accounts a ON a.id = k.account_id WHERE k.key = ${key}`; if (apiKey) { sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${apiKey.id}`.catch(() => {}); return { accountId: apiKey.account_id, keyId: apiKey.id, plan: apiKey.plan }; } return null; } export { resolveKey }; export function requireAuth(app: Elysia) { return app .derive(async ({ headers, cookie, set }) => { const authHeader = headers["authorization"] ?? ""; const bearer = authHeader.match(/^bearer\s+(.+)$/i)?.[1]?.trim(); const cookieKey = cookie?.pingql_key?.value; const key = bearer || cookieKey; if (!key) { set.status = 401; return { accountId: null as string | null, keyId: null as string | null }; } const resolved = await resolveKey(key); if (resolved) return { accountId: resolved.accountId, keyId: resolved.keyId }; 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" }; } }); } const COOKIE_OPTS = { httpOnly: true, secure: process.env.COOKIE_SECURE !== "false", sameSite: "none" as const, path: "/", domain: process.env.COOKIE_DOMAIN ?? ".pingql.com", maxAge: 60 * 60 * 24 * 30, // 30 days }; export const account = new Elysia({ prefix: "/account" }) .post("/login", async ({ body, cookie, set, request, error }) => { const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; if (!checkAuthRateLimit(ip, 10)) return error(429, { 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) return redir("/dashboard?error=invalid"); return { error: "Invalid account key" }; } cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); if ((body as any)._form) return redir("/dashboard/home"); return { ok: true }; }, { detail: { hide: true } }) .get("/logout", ({ cookie }) => { cookie.pingql_key.set({ value: "", ...COOKIE_OPTS, maxAge: 0 }); return redir("/dashboard"); }, { detail: { hide: true } }) .post("/register", async ({ body, cookie, request, set, error }) => { const ip = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown"; if (!checkAuthRateLimit(ip, 5)) return error(429, { error: "Too many registrations. Try again later." }); const key = generateKey(); const emailHash = (body as any).email ? hashEmail((body as any).email) : null; await sql`INSERT INTO accounts (key, email_hash) VALUES (${key}, ${emailHash})`; cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); // Form submission → redirect to welcome page showing the key if ((body as any)._form) return redir(`/dashboard/welcome?key=${encodeURIComponent(key)}`); return { key, email_registered: !!emailHash }; }) .use(requireAuth) .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, key, 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, }; }) .post("/email", async ({ accountId, body }) => { const emailHash = (body as any).email ? hashEmail((body as any).email) : null; await sql`UPDATE accounts SET email_hash = ${emailHash} WHERE id = ${accountId}`; if ((body as any)._form) return redir("/dashboard/settings"); return { ok: true }; }) .post("/reset-key", async ({ accountId, cookie, body }) => { const key = generateKey(); await sql`UPDATE accounts SET key = ${key} WHERE id = ${accountId}`; cookie.pingql_key.set({ value: key, ...COOKIE_OPTS }); if ((body as any)?._form) return redir("/dashboard/settings"); return { key, message: "Primary key rotated. Your old key is now invalid." }; }) .post("/keys", async ({ accountId, body }) => { const key = generateKey(); const [created] = await sql`INSERT INTO api_keys (key, account_id, label) VALUES (${key}, ${accountId}, ${(body as any).label}) RETURNING id`; if ((body as any)._form) return redir("/dashboard/settings"); return { key, id: created.id, label: (body as any).label }; }) .post("/keys/:id/delete", async ({ accountId, params }) => { await sql`DELETE FROM api_keys WHERE id = ${params.id} AND account_id = ${accountId}`; return redir("/dashboard/settings"); }) .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 }; });