forked from
joebasser.com/atmosphere-account
this repo has no description
1/**
2 * Admin allowlist + helpers.
3 *
4 * The list of admin DIDs is supplied via the `ADMIN_DIDS` env var
5 * (comma-separated). All `/admin/*` pages are gated by
6 * `routes/admin/_middleware.ts` which calls `requireAdmin`; admin API
7 * routes under `/api/admin/*` should call `requireAdminApi` directly
8 * because they need to return JSON 401/403 instead of an HTML redirect.
9 *
10 * Admins authenticate exactly like normal users (via the existing
11 * atproto OAuth flow); admin status is purely a server-side allowlist
12 * check on the resulting session DID.
13 */
14import { ADMIN_DIDS } from "./env.ts";
15
16export function isAdmin(did: string | null | undefined): boolean {
17 if (!did) return false;
18 return ADMIN_DIDS.includes(did);
19}
20
21/** Are any admins configured at all? Useful for hiding admin links from
22 * navigation in deployments that haven't opted in. */
23export function adminConfigured(): boolean {
24 return ADMIN_DIDS.length > 0;
25}
26
27interface AdminCtxLike {
28 state: { user: { did: string } | null };
29 req: Request;
30}
31
32/**
33 * Throws via redirect (302 → /oauth/login) when the request isn't from
34 * an admin. Page routes should `await requireAdmin(ctx)` at the top of
35 * their handler. Returns the verified admin DID on success.
36 */
37export function requireAdmin(ctx: AdminCtxLike): string {
38 const user = ctx.state.user;
39 if (!user) {
40 throw redirectResponse(loginUrl(ctx.req.url));
41 }
42 if (!isAdmin(user.did)) {
43 throw notFoundResponse();
44 }
45 return user.did;
46}
47
48/**
49 * API-shaped admin gate. Returns `{ ok: false, response }` so handlers
50 * can early-return; or `{ ok: true, did }` to proceed.
51 */
52export function requireAdminApi(
53 ctx: AdminCtxLike,
54):
55 | { ok: true; did: string }
56 | { ok: false; response: Response } {
57 const user = ctx.state.user;
58 if (!user) {
59 return {
60 ok: false,
61 response: jsonResponse(401, { error: "not_authenticated" }),
62 };
63 }
64 if (!isAdmin(user.did)) {
65 return { ok: false, response: jsonResponse(403, { error: "forbidden" }) };
66 }
67 return { ok: true, did: user.did };
68}
69
70function loginUrl(currentUrl: string): string {
71 try {
72 const url = new URL(currentUrl);
73 const next = url.pathname + url.search;
74 return `/oauth/login?next=${encodeURIComponent(next)}`;
75 } catch {
76 return "/oauth/login";
77 }
78}
79
80function redirectResponse(location: string): Response {
81 return new Response(null, { status: 303, headers: { location } });
82}
83
84/** Surface admin routes as 404 to anonymous/non-admin users so we don't
85 * leak that the URL exists. (Logged-in non-admins also get 404.) */
86function notFoundResponse(): Response {
87 return new Response("not found", { status: 404 });
88}
89
90function jsonResponse(status: number, body: unknown): Response {
91 return new Response(JSON.stringify(body), {
92 status,
93 headers: { "content-type": "application/json; charset=utf-8" },
94 });
95}