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`
`;
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("/");
});