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
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e8d6e69223b4b17359bfdcc4f27cf3a836e6f2c3 148 lines 4.5 kB view raw
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}