packages/server/src/routes/auth.ts 4.0 K raw
1
import { Hono } from "hono";
2
import type { Database } from "bun:sqlite";
3
import { createOAuthClient, OAUTH_SCOPE } from "../lib/oauth-client";
4
import { kvGet, kvSet, kvDel } from "../lib/db";
5
import {
6
	getSessionDid,
7
	setSessionCookie,
8
	clearSessionCookie,
9
	getReturnToCookie,
10
	clearReturnToCookie,
11
} from "../lib/session";
12
import type { Env } from "../env";
13
14
type Variables = { env: Env; db: Database };
15
16
const auth = new Hono<{ Variables: Variables }>();
17
18
// OAuth client metadata endpoint
19
auth.get("/client-metadata.json", (c) => {
20
	const env = c.get("env");
21
	const clientId = `${env.CLIENT_URL}/oauth/client-metadata.json`;
22
	const redirectUri = `${env.CLIENT_URL}/oauth/callback`;
23
24
	return c.json({
25
		client_id: clientId,
26
		client_name: env.CLIENT_NAME,
27
		client_uri: env.CLIENT_URL,
28
		redirect_uris: [redirectUri],
29
		grant_types: ["authorization_code", "refresh_token"],
30
		response_types: ["code"],
31
		scope: OAUTH_SCOPE,
32
		token_endpoint_auth_method: "none",
33
		application_type: "web",
34
		dpop_bound_access_tokens: true,
35
	});
36
});
37
38
// Start OAuth login flow
39
auth.get("/login", async (c) => {
40
	const env = c.get("env");
41
	const db = c.get("db");
42
43
	try {
44
		const handle = c.req.query("handle");
45
		if (!handle) {
46
			return c.redirect(`${env.CLIENT_URL}/?error=missing_handle`);
47
		}
48
49
		const client = createOAuthClient(db, env.CLIENT_URL, env.CLIENT_NAME);
50
		const authUrl = await client.authorize(handle, {
51
			scope: OAUTH_SCOPE,
52
		});
53
54
		return c.redirect(authUrl.toString());
55
	} catch (error) {
56
		console.error("Login error:", error);
57
		return c.redirect(`${env.CLIENT_URL}/?error=login_failed`);
58
	}
59
});
60
61
// OAuth callback handler
62
auth.get("/callback", async (c) => {
63
	const env = c.get("env");
64
	const db = c.get("db");
65
66
	try {
67
		const params = new URLSearchParams(c.req.url.split("?")[1] || "");
68
69
		if (params.get("error")) {
70
			const error = params.get("error");
71
			console.error("OAuth error:", error, params.get("error_description"));
72
			return c.redirect(
73
				`${env.CLIENT_URL}/?error=${encodeURIComponent(error!)}`,
74
			);
75
		}
76
77
		const client = createOAuthClient(db, env.CLIENT_URL, env.CLIENT_NAME);
78
		const { session } = await client.callback(params);
79
80
		// Resolve handle from DID
81
		let handle: string | undefined;
82
		try {
83
			const identity = await client.identityResolver.resolve(session.did);
84
			handle = identity.handle;
85
		} catch {
86
			// Handle resolution is best-effort
87
		}
88
89
		// Store handle alongside the session for quick lookup
90
		if (handle) {
91
			kvSet(db, `oauth_handle:${session.did}`, handle, 60 * 60 * 24 * 14);
92
		}
93
94
		setSessionCookie(c, session.did, env.CLIENT_URL);
95
96
		// If a subscribe flow set a return URL before initiating OAuth, honor it
97
		const returnTo = getReturnToCookie(c);
98
		clearReturnToCookie(c, env.CLIENT_URL);
99
100
		return c.redirect(returnTo ?? `${env.CLIENT_URL}/`);
101
	} catch (error) {
102
		console.error("Callback error:", error);
103
		return c.redirect(`${env.CLIENT_URL}/?error=callback_failed`);
104
	}
105
});
106
107
// Logout endpoint
108
auth.post("/logout", async (c) => {
109
	const env = c.get("env");
110
	const db = c.get("db");
111
	const did = getSessionDid(c);
112
113
	if (did) {
114
		try {
115
			const client = createOAuthClient(db, env.CLIENT_URL, env.CLIENT_NAME);
116
			await client.revoke(did);
117
		} catch (error) {
118
			console.error("Revoke error:", error);
119
		}
120
		kvDel(db, `oauth_handle:${did}`);
121
	}
122
123
	clearSessionCookie(c, env.CLIENT_URL);
124
	return c.json({ success: true });
125
});
126
127
// Check auth status
128
auth.get("/status", async (c) => {
129
	const env = c.get("env");
130
	const db = c.get("db");
131
	const did = getSessionDid(c);
132
133
	if (!did) {
134
		return c.json({ authenticated: false });
135
	}
136
137
	try {
138
		const client = createOAuthClient(db, env.CLIENT_URL, env.CLIENT_NAME);
139
		const session = await client.restore(did);
140
141
		const handle = kvGet(db, `oauth_handle:${session.did}`);
142
143
		return c.json({
144
			authenticated: true,
145
			did: session.did,
146
			handle: handle || undefined,
147
		});
148
	} catch (error) {
149
		console.error("Session restore failed:", error);
150
		clearSessionCookie(c, env.CLIENT_URL);
151
		return c.json({ authenticated: false });
152
	}
153
});
154
155
export default auth;