This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
app/apps/api/src/lib/auth.ts
T
groombook-engineer[bot] 6872342d8f fix(auth): resolve redirect loop and mount Better-Auth as sub-app (#144)
## Changes
- Replace toNodeHandler with auth.handler(c.req.raw) sub-app mount for Hono compatibility
- Add /api/auth/ path skip in authMiddleware and resolveStaffMiddleware
- Add OIDC_INTERNAL_BASE env var for split-horizon (hairpin NAT) URL resolution
- Replace render-time signIn.social() with LoginPage component (fixes redirect loop)
- Change auth-client baseURL to relative (empty string) for deployed environments
- Add POST /api/portal/appointments/:id/reschedule endpoint with session auth
- Add RescheduleFlow modal, PetForm component, and wire Dashboard/Appointments UI

## CTO Note
Auth fix is P0-critical. Portal mock data (UAT blocker) predates this PR and is tracked separately in GRO-218.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-28 22:10:50 +00:00

62 lines
2.2 KiB
TypeScript

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { genericOAuth } from "better-auth/plugins";
import { getDb } from "@groombook/db";
const OIDC_ISSUER = process.env.OIDC_ISSUER;
const OIDC_INTERNAL_BASE = process.env.OIDC_INTERNAL_BASE; // e.g. http://authentik-server.auth.svc.cluster.local
const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID;
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET;
const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET;
const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
if (!BETTER_AUTH_SECRET && process.env.AUTH_DISABLED !== "true") {
throw new Error(
"[FATAL] BETTER_AUTH_SECRET environment variable is required when auth is enabled"
);
}
export const auth = betterAuth({
database: drizzleAdapter(getDb(), {
provider: "pg",
}),
secret: BETTER_AUTH_SECRET,
baseURL: BETTER_AUTH_URL,
plugins: [
genericOAuth({
config: [
{
providerId: "authentik",
clientId: OIDC_CLIENT_ID ?? "",
clientSecret: OIDC_CLIENT_SECRET ?? "",
// When OIDC_INTERNAL_BASE is set, use explicit URLs to avoid hairpin NAT:
// - authorizationUrl: external (browser redirect, no server-side fetch)
// - tokenUrl/userInfoUrl: internal (server-to-server, avoids hairpin)
// When not set, fall back to discoveryUrl for local dev.
...(OIDC_INTERNAL_BASE
? {
authorizationUrl: `${new URL(OIDC_ISSUER!).origin}/application/o/authorize/`,
tokenUrl: `${OIDC_INTERNAL_BASE}/application/o/token/`,
userInfoUrl: `${OIDC_INTERNAL_BASE}/application/o/userinfo/`,
}
: {
discoveryUrl: OIDC_ISSUER
? `${OIDC_ISSUER}/.well-known/openid-configuration`
: undefined,
}),
scopes: ["openid", "profile", "email"],
},
],
}),
],
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // 1 day
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes
},
},
trustedOrigins: [process.env.CORS_ORIGIN ?? "http://localhost:5173"],
});