From 41dff6f0e20e517424778c41c96ae7d9e4d92272 Mon Sep 17 00:00:00 2001 From: "groombook-cto[bot]" <269737991+groombook-cto[bot]@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:07:41 +0000 Subject: [PATCH] fix(GRO-563): stabilize OAuth login - upgrade better-auth, fix service worker, add 503 handling Phase 1 Better Auth stabilization: - Upgrade better-auth to ^1.5.6 in apps/web (matches api) - Switch OAuth state to cookie storage (BA v1.5+ requirement) - Remove manual redirectURI overrides - Exclude /api/auth/* from service worker caching - Add 503 error handling when auth not configured - Display login errors inline on login page - Update infra submodule with social auth env vars Closes GRO-563 Co-Authored-By: Paperclip --- apps/api/src/index.ts | 8 +++++++- apps/api/src/lib/auth.ts | 7 +++---- apps/api/src/middleware/auth.ts | 10 ++++++++-- apps/web/package.json | 2 +- apps/web/src/App.tsx | 16 +++++++++++++++- apps/web/vite.config.ts | 4 ++-- infra | 2 +- pnpm-lock.yaml | 2 +- 8 files changed, 38 insertions(+), 13 deletions(-) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 1b146b9..6cf62ac 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -105,7 +105,13 @@ api.use("*", resolveStaffMiddleware); // Better-Auth handler — mounted as sub-app to handle all /api/auth/* routes // authMiddleware and resolveStaffMiddleware both skip /api/auth/ paths const authRouter = new Hono(); -authRouter.all("/*", (c) => getAuth().handler(c.req.raw)); +authRouter.all("/*", (c) => { + try { + return getAuth().handler(c.req.raw); + } catch { + return c.json({ error: "Authentication not configured" }, 503); + } +}); api.route("/auth", authRouter); // ── Role guards ──────────────────────────────────────────────────────────────── diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts index f77dcbf..fc9f2e2 100644 --- a/apps/api/src/lib/auth.ts +++ b/apps/api/src/lib/auth.ts @@ -170,8 +170,6 @@ export async function initAuth(): Promise { const hasGoogle = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); const hasGitHub = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET); - const callbackBase = `${BETTER_AUTH_URL}/api/auth/callback`; - // Build Better-Auth instance using resolved config authInstance = betterAuth({ database: drizzleAdapter(db, { @@ -179,6 +177,9 @@ export async function initAuth(): Promise { }), secret: BETTER_AUTH_SECRET, baseURL: BETTER_AUTH_URL, + account: { + storeStateStrategy: "cookie" as const, + }, plugins: [ genericOAuth({ config: [ @@ -205,14 +206,12 @@ export async function initAuth(): Promise { google: { clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, - redirectURI: `${callbackBase}/google`, }, } : {}), ...(hasGitHub ? { github: { clientId: process.env.GITHUB_CLIENT_ID!, clientSecret: process.env.GITHUB_CLIENT_SECRET!, - redirectURI: `${callbackBase}/github`, }, } : {}), }, diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 1417614..906f505 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -23,7 +23,6 @@ if (process.env.AUTH_DISABLED === "true") { } export const authMiddleware: MiddlewareHandler = async (c, next) => { - // Better-Auth's own routes handle their own auth (OAuth callbacks, session mgmt) if (c.req.path.startsWith("/api/auth/")) { await next(); return; @@ -37,7 +36,14 @@ export const authMiddleware: MiddlewareHandler = async (c, next) => { return; } - const session = await getAuth().api.getSession({ + let auth; + try { + auth = getAuth(); + } catch { + return c.json({ error: "Authentication not configured" }, 503); + } + + const session = await auth.api.getSession({ headers: c.req.raw.headers, }); diff --git a/apps/web/package.json b/apps/web/package.json index 2cf5416..3c9d044 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,7 +15,7 @@ "dependencies": { "@groombook/types": "workspace:*", "@tailwindcss/vite": "^4.2.2", - "better-auth": "^1.0.0", + "better-auth": "^1.5.6", "lucide-react": "^0.577.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index efaefd3..bf34c03 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -23,17 +23,26 @@ import { useSession, signIn } from "./lib/auth-client.js"; function LoginPage() { const [isLoading, setIsLoading] = useState(false); const [providers, setProviders] = useState([]); + const [error, setError] = useState(null); useEffect(() => { fetch("/api/auth/providers") .then((r) => r.json()) .then((data) => setProviders(data.providers ?? [])) .catch(() => setProviders([])); + const params = new URLSearchParams(window.location.search); + const authError = params.get("error"); + if (authError) setError(authError.replace(/_/g, " ")); }, []); const handleSocialLogin = async (provider: string) => { setIsLoading(true); - await signIn.social({ provider, callbackURL: window.location.origin }); + setError(null); + const result = await signIn.social({ provider, callbackURL: window.location.origin }); + if (result?.error) { + setError(result.error.message ?? "Sign-in failed"); + setIsLoading(false); + } }; const isGoogle = providers.includes("google"); @@ -65,6 +74,11 @@ function LoginPage() {

Sign in to continue

+ {error && ( +
+ {error} +
+ )} {isGoogle && (