WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import { isProgrammingError } from "./errors.js";
2
3export type WebSession =
4 | { authenticated: false }
5 | { authenticated: true; did: string; handle: string };
6
7/**
8 * Fetches the current session from AppView by forwarding the browser's
9 * atbb_session cookie in a server-to-server call.
10 *
11 * Returns unauthenticated if no cookie is present, AppView is unreachable,
12 * or the session is invalid.
13 */
14export async function getSession(
15 appviewUrl: string,
16 cookieHeader?: string
17): Promise<WebSession> {
18 if (!cookieHeader || !cookieHeader.includes("atbb_session=")) {
19 return { authenticated: false };
20 }
21
22 try {
23 const res = await fetch(`${appviewUrl}/api/auth/session`, {
24 headers: { Cookie: cookieHeader },
25 });
26
27 if (!res.ok) {
28 if (res.status !== 401) {
29 console.error("getSession: unexpected non-ok status from AppView", {
30 operation: "GET /api/auth/session",
31 status: res.status,
32 });
33 }
34 return { authenticated: false };
35 }
36
37 const data = (await res.json()) as Record<string, unknown>;
38
39 if (
40 data.authenticated === true &&
41 typeof data.did === "string" &&
42 typeof data.handle === "string"
43 ) {
44 return { authenticated: true, did: data.did, handle: data.handle };
45 }
46
47 return { authenticated: false };
48 } catch (error) {
49 if (isProgrammingError(error)) throw error;
50 console.error(
51 "getSession: network error — treating as unauthenticated",
52 {
53 operation: "GET /api/auth/session",
54 error: error instanceof Error ? error.message : String(error),
55 }
56 );
57 return { authenticated: false };
58 }
59}
60
61/**
62 * Extended session type that includes the user's role permissions.
63 * Used on pages that need to conditionally render moderation UI.
64 */
65export type WebSessionWithPermissions =
66 | { authenticated: false; permissions: Set<string> }
67 | { authenticated: true; did: string; handle: string; permissions: Set<string> };
68
69/**
70 * Like getSession(), but also fetches the user's role permissions from
71 * GET /api/admin/members/me. Use on pages that need to render mod buttons.
72 *
73 * Returns empty permissions on network errors or when user has no membership.
74 * Never throws — always returns a usable session.
75 */
76export async function getSessionWithPermissions(
77 appviewUrl: string,
78 cookieHeader?: string
79): Promise<WebSessionWithPermissions> {
80 const session = await getSession(appviewUrl, cookieHeader);
81
82 if (!session.authenticated) {
83 return { authenticated: false, permissions: new Set() };
84 }
85
86 let permissions = new Set<string>();
87 try {
88 const res = await fetch(`${appviewUrl}/api/admin/members/me`, {
89 headers: { Cookie: cookieHeader! },
90 });
91
92 if (res.ok) {
93 const data = (await res.json()) as Record<string, unknown>;
94 if (Array.isArray(data.permissions)) {
95 permissions = new Set(data.permissions as string[]);
96 }
97 } else if (res.status !== 404) {
98 // 404 = no membership = expected for guests, no log needed
99 console.error(
100 "getSessionWithPermissions: unexpected status from members/me",
101 {
102 operation: "GET /api/admin/members/me",
103 did: session.did,
104 status: res.status,
105 }
106 );
107 }
108 } catch (error) {
109 if (isProgrammingError(error)) throw error;
110 console.error(
111 "getSessionWithPermissions: network error — continuing with empty permissions",
112 {
113 operation: "GET /api/admin/members/me",
114 did: session.did,
115 error: error instanceof Error ? error.message : String(error),
116 }
117 );
118 }
119
120 return { ...session, permissions };
121}
122
123/** Returns true if the session grants permission to lock/unlock topics. */
124export function canLockTopics(auth: WebSessionWithPermissions): boolean {
125 return (
126 auth.authenticated &&
127 (auth.permissions.has("space.atbb.permission.lockTopics") ||
128 auth.permissions.has("*"))
129 );
130}
131
132/** Returns true if the session grants permission to hide/unhide posts. */
133export function canModeratePosts(auth: WebSessionWithPermissions): boolean {
134 return (
135 auth.authenticated &&
136 (auth.permissions.has("space.atbb.permission.moderatePosts") ||
137 auth.permissions.has("*"))
138 );
139}
140
141/** Returns true if the session grants permission to ban/unban users. */
142export function canBanUsers(auth: WebSessionWithPermissions): boolean {
143 return (
144 auth.authenticated &&
145 (auth.permissions.has("space.atbb.permission.banUsers") ||
146 auth.permissions.has("*"))
147 );
148}