Files
STTracker/apps/server/src/routes/auth.ts
T
Lucas WintherandClaude Opus 5.5 6963e297b0 feat(server): REST API, auth, sources, changes, export
createApp() serves /api/v1 from a typed route table (zod bodies and queries), with
session-cookie and Bearer app-token auth, a CSRF header for cookie writes, login/register
rate limiting, body caps, JSON error mapping, OpenAPI generation and a 404 /ingest mount.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:15:01 +01:00

77 lines
3.0 KiB
TypeScript

import { createAppToken, createInvite, listAppTokens, login, logout, registerUser, revokeAppToken } from "@sttracker/core/auth";
import { z } from "zod";
import { clearSessionCookie, json, noContent, route, sessionCookie } from "../http.ts";
const credentials = z.object({
username: z.string().min(1).max(64),
password: z.string().min(8).max(1024),
});
export const authRoutes = [
route({
method: "POST",
path: "auth/register",
summary: "Create an account. The first user becomes admin; later ones need an invite code.",
auth: "none",
rateLimited: true,
body: credentials.extend({ displayName: z.string().min(1).max(100).optional(), inviteCode: z.string().max(200).optional() }).strict(),
handler: async ({ deps, body }) => json(await registerUser(deps.db, body), 201),
}),
route({
method: "POST",
path: "auth/login",
summary: "Sign in; sets the stt_session cookie.",
auth: "none",
rateLimited: true,
body: z.object({ username: z.string().max(64), password: z.string().max(1024) }).strict(),
handler: async ({ deps, body }) => {
const { user, sessionToken } = await login(deps.db, body.username, body.password);
return json(user, 200, { "set-cookie": sessionCookie(sessionToken, deps.publicUrl) });
},
}),
route({
method: "POST",
path: "auth/logout",
summary: "End the current session and clear the cookie.",
auth: "session",
handler: async ({ deps, sessionToken }) => {
if (sessionToken) await logout(deps.db, sessionToken);
return new Response(null, { status: 204, headers: { "set-cookie": clearSessionCookie(deps.publicUrl) } });
},
}),
route({ method: "GET", path: "me", summary: "The signed-in user.", auth: "any:read", handler: async ({ user }) => user }),
route({
method: "GET",
path: "me/tokens",
summary: "List your app tokens (metadata only).",
auth: "session",
handler: async ({ deps, user }) => listAppTokens(deps.db, user.id),
}),
route({
method: "POST",
path: "me/tokens",
summary: "Create an app token. The token is returned once.",
auth: "session",
body: z.object({ name: z.string().min(1).max(100), scopes: z.array(z.enum(["read", "write", "ingest"])).min(1) }).strict(),
handler: async ({ deps, user, body }) => json(await createAppToken(deps.db, user.id, body.name, [...new Set(body.scopes)]), 201),
}),
route({
method: "DELETE",
path: "me/tokens/:id",
summary: "Revoke an app token.",
auth: "session",
handler: async ({ deps, user, params }) => {
await revokeAppToken(deps.db, user.id, params.id!);
return noContent();
},
}),
route({
method: "POST",
path: "admin/invites",
summary: "Create an invite code (admin). The code is returned once.",
auth: "session",
body: z.object({ ttlDays: z.number().int().min(1).max(365).optional() }).strict().optional(),
handler: async ({ deps, user, body }) => json({ code: await createInvite(deps.db, user.id, body?.ttlDays) }, 201),
}),
];