this repo has no description
1import { Hono } from "hono";
2import { getCookie, setCookie, deleteCookie } from "hono/cookie";
3import { html } from "hono/html";
4import {
5 getOAuthClient,
6 getClientMetadata,
7 getJwks,
8 deleteSession,
9} from "../lib/oauth";
10import { layout } from "../views/layouts/main";
11import { csrfField } from "../lib/csrf";
12import type { AppVariables } from "../types";
13
14export const authRoutes = new Hono<{ Variables: AppVariables }>();
15
16// Client metadata endpoint (required for OAuth)
17authRoutes.get("/client-metadata.json", async (c) => {
18 try {
19 const metadata = await getClientMetadata();
20 return c.json(metadata);
21 } catch (error) {
22 console.error("Error getting client metadata:", error);
23 return c.json({ error: "Failed to get client metadata" }, 500);
24 }
25});
26
27// JWKS endpoint (required for confidential clients)
28authRoutes.get("/jwks.json", async (c) => {
29 try {
30 const jwks = await getJwks();
31 return c.json(jwks);
32 } catch (error) {
33 console.error("Error getting JWKS:", error);
34 return c.json({ error: "Failed to get JWKS" }, 500);
35 }
36});
37
38// Login page
39authRoutes.get("/login", async (c) => {
40 const error = c.req.query("error");
41 const csrfToken = c.get("csrfToken") as string;
42
43 const content = html`
44 <div class="auth-form">
45 <h1>Login with Bluesky</h1>
46
47 ${
48 error
49 ? html`
50 <div class="error-message">
51 ${
52 error === "handle_required"
53 ? "Please enter your handle or DID."
54 : error === "authorization_failed"
55 ? "Authorization failed. Please try again."
56 : error === "callback_failed"
57 ? "Login failed. Please try again."
58 : "An error occurred. Please try again."
59 }
60 </div>
61 `
62 : ""
63 }
64
65 <form action="/auth/login" method="POST">
66 ${csrfField(csrfToken)}
67 <div class="form-group">
68 <label for="handle">Handle or DID</label>
69 <input
70 type="text"
71 id="handle"
72 name="handle"
73 placeholder="yourname.bsky.social"
74 required
75 autocomplete="username"
76 autocapitalize="none"
77 />
78 <small
79 >Enter your Bluesky handle (e.g., yourname.bsky.social) or
80 DID</small
81 >
82 </div>
83 <button type="submit" class="btn btn-primary">Login</button>
84 </form>
85 </div>
86 `;
87
88 return c.html(layout(content, { title: "Login - sitebase" }));
89});
90
91// Handle login form submission
92authRoutes.post("/login", async (c) => {
93 const body = await c.req.parseBody();
94 let handle = body.handle as string;
95
96 if (!handle) {
97 return c.redirect("/auth/login?error=handle_required");
98 }
99
100 // Trim and normalize handle
101 handle = handle.trim().toLowerCase();
102
103 // Remove @ prefix if present
104 if (handle.startsWith("@")) {
105 handle = handle.slice(1);
106 }
107
108 try {
109 const client = await getOAuthClient();
110 const url = await client.authorize(handle, {
111 scope: "atproto transition:generic",
112 });
113
114 return c.redirect(url.toString());
115 } catch (error) {
116 console.error("Login error:", error);
117 return c.redirect("/auth/login?error=authorization_failed");
118 }
119});
120
121// OAuth callback
122authRoutes.get("/callback", async (c) => {
123 const url = new URL(c.req.url);
124 const params = url.searchParams;
125
126 // Check for error from authorization server
127 const error = params.get("error");
128 if (error) {
129 console.error("OAuth error:", error, params.get("error_description"));
130 return c.redirect("/auth/login?error=callback_failed");
131 }
132
133 try {
134 const client = await getOAuthClient();
135 const { session } = await client.callback(params);
136
137 // Store the DID in a cookie for session management
138 // The actual OAuth session is stored in the database by the OAuth client
139 setCookie(c, "session", session.did, {
140 httpOnly: true,
141 secure:
142 process.env.NODE_ENV === "production" ||
143 process.env.PUBLIC_URL?.startsWith("https"),
144 sameSite: "Lax",
145 maxAge: 60 * 60 * 24 * 7, // 7 days
146 path: "/",
147 });
148
149 return c.redirect("/");
150 } catch (error) {
151 console.error("Callback error:", error);
152 return c.redirect("/auth/login?error=callback_failed");
153 }
154});
155
156// Logout
157authRoutes.get("/logout", async (c) => {
158 const did = getCookie(c, "session");
159
160 if (did) {
161 try {
162 // Delete the OAuth session from the database
163 await deleteSession(did);
164 } catch (error) {
165 console.error("Error deleting session:", error);
166 }
167 }
168
169 deleteCookie(c, "session", { path: "/" });
170 return c.redirect("/");
171});