116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
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 };
|
|
});
|