import { Hono } from "hono"; import { getCookie, setCookie, deleteCookie } from "hono/cookie"; import { html } from "hono/html"; import { getOAuthClient, getClientMetadata, getJwks, deleteSession, } from "../lib/oauth"; import { layout } from "../views/layouts/main"; import { csrfField } from "../lib/csrf"; import type { AppVariables } from "../types"; export const authRoutes = new Hono<{ Variables: AppVariables }>(); // Client metadata endpoint (required for OAuth) authRoutes.get("/client-metadata.json", async (c) => { try { const metadata = await getClientMetadata(); return c.json(metadata); } catch (error) { console.error("Error getting client metadata:", error); return c.json({ error: "Failed to get client metadata" }, 500); } }); // JWKS endpoint (required for confidential clients) authRoutes.get("/jwks.json", async (c) => { try { const jwks = await getJwks(); return c.json(jwks); } catch (error) { console.error("Error getting JWKS:", error); return c.json({ error: "Failed to get JWKS" }, 500); } }); // Login page authRoutes.get("/login", async (c) => { const error = c.req.query("error"); const csrfToken = c.get("csrfToken") as string; const content = html`

Login with Bluesky

${ error ? html`
${ error === "handle_required" ? "Please enter your handle or DID." : error === "authorization_failed" ? "Authorization failed. Please try again." : error === "callback_failed" ? "Login failed. Please try again." : "An error occurred. Please try again." }
` : "" }
${csrfField(csrfToken)}
Enter your Bluesky handle (e.g., yourname.bsky.social) or DID
`; return c.html(layout(content, { title: "Login - sitebase" })); }); // Handle login form submission authRoutes.post("/login", async (c) => { const body = await c.req.parseBody(); let handle = body.handle as string; if (!handle) { return c.redirect("/auth/login?error=handle_required"); } // Trim and normalize handle handle = handle.trim().toLowerCase(); // Remove @ prefix if present if (handle.startsWith("@")) { handle = handle.slice(1); } try { const client = await getOAuthClient(); const url = await client.authorize(handle, { scope: "atproto transition:generic", }); return c.redirect(url.toString()); } catch (error) { console.error("Login error:", error); return c.redirect("/auth/login?error=authorization_failed"); } }); // OAuth callback authRoutes.get("/callback", async (c) => { const url = new URL(c.req.url); const params = url.searchParams; // Check for error from authorization server const error = params.get("error"); if (error) { console.error("OAuth error:", error, params.get("error_description")); return c.redirect("/auth/login?error=callback_failed"); } try { const client = await getOAuthClient(); const { session } = await client.callback(params); // Store the DID in a cookie for session management // The actual OAuth session is stored in the database by the OAuth client setCookie(c, "session", session.did, { httpOnly: true, secure: process.env.NODE_ENV === "production" || process.env.PUBLIC_URL?.startsWith("https"), sameSite: "Lax", maxAge: 60 * 60 * 24 * 7, // 7 days path: "/", }); return c.redirect("/"); } catch (error) { console.error("Callback error:", error); return c.redirect("/auth/login?error=callback_failed"); } }); // Logout authRoutes.get("/logout", async (c) => { const did = getCookie(c, "session"); if (did) { try { // Delete the OAuth session from the database await deleteSession(did); } catch (error) { console.error("Error deleting session:", error); } } deleteCookie(c, "session", { path: "/" }); return c.redirect("/"); });