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 { getCookie, deleteCookie } from "hono/cookie";
2import type { Context, Next } from "hono";
3import { Agent } from "@atproto/api";
4import type { AppContext } from "../lib/app-context.js";
5import { restoreOAuthSession } from "../lib/session.js";
6import type { AuthenticatedUser, Variables } from "../types.js";
7
8/**
9 * Helper to restore OAuth session from cookie and create an Agent.
10 *
11 * Delegates to the shared `restoreOAuthSession` for cookie lookup and
12 * OAuth session restoration, then enriches the result with an Agent,
13 * handle, and PDS URL to produce an AuthenticatedUser.
14 *
15 * Returns null if session doesn't exist or is expired (expected).
16 * Throws on unexpected errors (network failures, etc.) that should bubble up.
17 */
18async function restoreSession(ctx: AppContext, cookieToken: string): Promise<AuthenticatedUser | null> {
19 const result = await restoreOAuthSession(ctx, cookieToken);
20 if (!result) {
21 return null;
22 }
23
24 const { oauthSession, cookieSession } = result;
25
26 // Create Agent from OAuth session
27 // The library's OAuthSession implements the fetch handler with DPoP
28 const agent = new Agent(oauthSession);
29
30 // Get handle from cookie session (fetched during login callback)
31 // Fall back to DID if handle wasn't stored
32 const handle = cookieSession.handle || oauthSession.did;
33
34 const user: AuthenticatedUser = {
35 did: oauthSession.did,
36 handle,
37 pdsUrl: oauthSession.serverMetadata.issuer, // PDS URL from server metadata
38 agent,
39 };
40
41 return user;
42}
43
44/**
45 * Require authentication middleware.
46 *
47 * Validates session cookie and attaches authenticated user to context.
48 * Returns 401 if session is missing or invalid.
49 *
50 * Usage:
51 * app.post('/api/posts', requireAuth(ctx), async (c) => {
52 * const user = c.get('user'); // Guaranteed to exist
53 * const agent = user.agent; // Pre-configured Agent with DPoP
54 * });
55 */
56export function requireAuth(ctx: AppContext) {
57 return async (c: Context<{ Variables: Variables }>, next: Next) => {
58 const sessionToken = getCookie(c, "atbb_session");
59
60 if (!sessionToken) {
61 return c.json({ error: "Authentication required" }, 401);
62 }
63
64 try {
65 const user = await restoreSession(ctx, sessionToken);
66
67 if (!user) {
68 return c.json({ error: "Invalid or expired session" }, 401);
69 }
70
71 // Attach user to context
72 c.set("user", user);
73
74 await next();
75 } catch (error) {
76 ctx.logger.error("Authentication middleware error", {
77 path: c.req.path,
78 error: error instanceof Error ? error.message : String(error),
79 });
80
81 return c.json(
82 {
83 error: "Authentication failed. Please try again.",
84 },
85 500
86 );
87 }
88 };
89}
90
91/**
92 * Optional authentication middleware.
93 *
94 * Validates session if present, but doesn't return 401 if missing.
95 * Useful for endpoints that work for both authenticated and unauthenticated users.
96 *
97 * Usage:
98 * app.get('/api/posts/:id', optionalAuth(ctx), async (c) => {
99 * const user = c.get('user'); // May be undefined
100 * if (user) {
101 * // Show edit buttons, etc.
102 * }
103 * });
104 */
105export function optionalAuth(ctx: AppContext) {
106 return async (c: Context<{ Variables: Variables }>, next: Next) => {
107 const sessionToken = getCookie(c, "atbb_session");
108
109 if (!sessionToken) {
110 await next();
111 return;
112 }
113
114 try {
115 const user = await restoreSession(ctx, sessionToken);
116
117 if (user) {
118 c.set("user", user);
119 } else {
120 // Session is invalid/expired - clean up the cookie
121 deleteCookie(c, "atbb_session");
122 }
123 } catch (error) {
124 // restoreSession now throws on unexpected errors only
125 // Log the unexpected error but don't fail the request
126 ctx.logger.warn("Unexpected error during optional auth", {
127 path: c.req.path,
128 error: error instanceof Error ? error.message : String(error),
129 });
130 // Clean up potentially corrupted cookie
131 deleteCookie(c, "atbb_session");
132 }
133
134 await next();
135 };
136}