CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
auth.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 06d5ffe | 1 | /** |
| 2 | * Auth middleware — reads session cookie, injects user into context. | |
| 05b973e | 3 | * Uses in-memory session cache to avoid DB roundtrip on every request. |
| 06d5ffe | 4 | */ |
| 5 | ||
| 6 | import { createMiddleware } from "hono/factory"; | |
| 7 | import { getCookie } from "hono/cookie"; | |
| 8 | import { eq, gt } from "drizzle-orm"; | |
| 9 | import { db } from "../db"; | |
| 10 | import { sessions, users } from "../db/schema"; | |
| 11 | import type { User } from "../db/schema"; | |
| 05b973e | 12 | import { sessionCache } from "../lib/cache"; |
| 06d5ffe | 13 | |
| 14 | export type AuthEnv = { | |
| 15 | Variables: { | |
| 16 | user: User | null; | |
| 17 | }; | |
| 18 | }; | |
| 19 | ||
| 20 | /** | |
| 21 | * Soft auth — sets c.get("user") to the current user or null. | |
| 22 | * Does NOT block unauthenticated requests. | |
| 05b973e | 23 | * Caches session->user mapping for 2 minutes to avoid DB roundtrip per request. |
| 06d5ffe | 24 | */ |
| 25 | export const softAuth = createMiddleware<AuthEnv>(async (c, next) => { | |
| 26 | const token = getCookie(c, "session"); | |
| 27 | if (!token) { | |
| 28 | c.set("user", null); | |
| 29 | return next(); | |
| 30 | } | |
| 31 | ||
| 05b973e | 32 | // Check session cache first |
| 33 | const cachedUser = sessionCache.get(token) as User | null | undefined; | |
| 34 | if (cachedUser !== undefined) { | |
| 35 | c.set("user", cachedUser); | |
| 36 | return next(); | |
| 37 | } | |
| 38 | ||
| 06d5ffe | 39 | try { |
| 40 | const [session] = await db | |
| 41 | .select() | |
| 42 | .from(sessions) | |
| 43 | .where(eq(sessions.token, token)) | |
| 44 | .limit(1); | |
| 45 | ||
| 46 | if (!session || new Date(session.expiresAt) < new Date()) { | |
| 05b973e | 47 | sessionCache.set(token, null as any); |
| 06d5ffe | 48 | c.set("user", null); |
| 49 | return next(); | |
| 50 | } | |
| 51 | ||
| 52 | const [user] = await db | |
| 53 | .select() | |
| 54 | .from(users) | |
| 55 | .where(eq(users.id, session.userId)) | |
| 56 | .limit(1); | |
| 57 | ||
| 05b973e | 58 | // Cache the result (user or null) |
| 59 | sessionCache.set(token, (user || null) as any); | |
| 06d5ffe | 60 | c.set("user", user || null); |
| 61 | } catch { | |
| 62 | c.set("user", null); | |
| 63 | } | |
| 64 | ||
| 65 | return next(); | |
| 66 | }); | |
| 67 | ||
| 68 | /** | |
| 69 | * Hard auth — redirects to /login if not authenticated. | |
| 70 | */ | |
| 71 | export const requireAuth = createMiddleware<AuthEnv>(async (c, next) => { | |
| 72 | const token = getCookie(c, "session"); | |
| 73 | if (!token) { | |
| 74 | return c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`); | |
| 75 | } | |
| 76 | ||
| 77 | try { | |
| 78 | const [session] = await db | |
| 79 | .select() | |
| 80 | .from(sessions) | |
| 81 | .where(eq(sessions.token, token)) | |
| 82 | .limit(1); | |
| 83 | ||
| 84 | if (!session || new Date(session.expiresAt) < new Date()) { | |
| 85 | return c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`); | |
| 86 | } | |
| 87 | ||
| 88 | const [user] = await db | |
| 89 | .select() | |
| 90 | .from(users) | |
| 91 | .where(eq(users.id, session.userId)) | |
| 92 | .limit(1); | |
| 93 | ||
| 94 | if (!user) { | |
| 95 | return c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`); | |
| 96 | } | |
| 97 | ||
| 98 | c.set("user", user); | |
| 99 | } catch { | |
| 100 | return c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`); | |
| 101 | } | |
| 102 | ||
| 103 | return next(); | |
| 104 | }); |