From 444fa1116a2376acd89e9477b931e46be2a9a6bc Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 15 Feb 2026 19:41:08 -0600 Subject: [PATCH] fix(#410): align BetterAuth basePath and auth client with NestJS routing BetterAuth defaulted basePath to /api/auth but NestJS controller routes to /auth/* (no global prefix). The auth client also pointed at the web frontend origin instead of the API server, and LoginButton used a nonexistent GET /auth/signin/authentik endpoint. - Set basePath: "/auth" in BetterAuth server config - Point auth client baseURL to API_BASE_URL with matching basePath - Add genericOAuthClient plugin to auth client - Use signIn.oauth2({ providerId: "authentik" }) in LoginButton Co-Authored-By: Claude Opus 4.6 --- apps/api/src/auth/auth.config.ts | 1 + .../src/components/auth/LoginButton.test.tsx | 28 ++++++++++--------- apps/web/src/components/auth/LoginButton.tsx | 8 +++--- apps/web/src/lib/auth-client.ts | 23 +++++---------- 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/apps/api/src/auth/auth.config.ts b/apps/api/src/auth/auth.config.ts index e07b2e4..c3d0f78 100644 --- a/apps/api/src/auth/auth.config.ts +++ b/apps/api/src/auth/auth.config.ts @@ -83,6 +83,7 @@ export function createAuth(prisma: PrismaClient) { validateOidcConfig(); return betterAuth({ + basePath: "/auth", database: prismaAdapter(prisma, { provider: "postgresql", }), diff --git a/apps/web/src/components/auth/LoginButton.test.tsx b/apps/web/src/components/auth/LoginButton.test.tsx index 96fc93d..d36fe7c 100644 --- a/apps/web/src/components/auth/LoginButton.test.tsx +++ b/apps/web/src/components/auth/LoginButton.test.tsx @@ -3,20 +3,19 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { LoginButton } from "./LoginButton"; -// Mock window.location -const mockLocation = { - href: "", - assign: vi.fn(), -}; -Object.defineProperty(window, "location", { - value: mockLocation, - writable: true, -}); +const { mockOAuth2 } = vi.hoisted(() => ({ + mockOAuth2: vi.fn(), +})); + +vi.mock("@/lib/auth-client", () => ({ + signIn: { + oauth2: mockOAuth2, + }, +})); describe("LoginButton", (): void => { beforeEach((): void => { - mockLocation.href = ""; - mockLocation.assign.mockClear(); + mockOAuth2.mockClear(); }); it("should render sign in button", (): void => { @@ -25,14 +24,17 @@ describe("LoginButton", (): void => { expect(button).toBeInTheDocument(); }); - it("should redirect to OIDC endpoint on click", async (): Promise => { + it("should initiate OAuth2 sign-in on click", async (): Promise => { const user = userEvent.setup(); render(); const button = screen.getByRole("button", { name: /sign in/i }); await user.click(button); - expect(mockLocation.assign).toHaveBeenCalledWith("http://localhost:3001/auth/signin/authentik"); + expect(mockOAuth2).toHaveBeenCalledWith({ + providerId: "authentik", + callbackURL: "/", + }); }); it("should have proper styling", (): void => { diff --git a/apps/web/src/components/auth/LoginButton.tsx b/apps/web/src/components/auth/LoginButton.tsx index 8c293ed..bc8c5dd 100644 --- a/apps/web/src/components/auth/LoginButton.tsx +++ b/apps/web/src/components/auth/LoginButton.tsx @@ -1,13 +1,13 @@ "use client"; import { Button } from "@mosaic/ui"; -import { API_BASE_URL } from "@/lib/config"; +import { signIn } from "@/lib/auth-client"; export function LoginButton(): React.JSX.Element { const handleLogin = (): void => { - // Redirect to the backend OIDC authentication endpoint - // BetterAuth will handle the OIDC flow and redirect back to the callback - window.location.assign(`${API_BASE_URL}/auth/signin/authentik`); + // Use BetterAuth's genericOAuth client to initiate the OIDC flow. + // This POSTs to /auth/sign-in/oauth2 and follows the returned redirect URL. + void signIn.oauth2({ providerId: "authentik", callbackURL: "/" }); }; return ( diff --git a/apps/web/src/lib/auth-client.ts b/apps/web/src/lib/auth-client.ts index f0213ca..393fb11 100644 --- a/apps/web/src/lib/auth-client.ts +++ b/apps/web/src/lib/auth-client.ts @@ -7,20 +7,16 @@ * - Automatic token refresh */ import { createAuthClient } from "better-auth/react"; -// Note: Credentials plugin import removed - better-auth has built-in credentials support +import { genericOAuthClient } from "better-auth/client/plugins"; +import { API_BASE_URL } from "./config"; /** - * Auth client instance configured for Jarvis. + * Auth client instance configured for Mosaic Stack. */ export const authClient = createAuthClient({ - // Base URL for auth API - baseURL: - typeof window !== "undefined" - ? window.location.origin - : (process.env.BETTER_AUTH_URL ?? "http://localhost:3042"), - - // Plugins can be added here when needed - plugins: [], + baseURL: API_BASE_URL, + basePath: "/auth", + plugins: [genericOAuthClient()], }); /** @@ -36,12 +32,7 @@ export const { signIn, signOut, useSession, getSession } = authClient; * and the default BetterAuth client expects email. */ export async function signInWithCredentials(username: string, password: string): Promise { - const baseURL = - typeof window !== "undefined" - ? window.location.origin - : (process.env.BETTER_AUTH_URL ?? "http://localhost:3042"); - - const response = await fetch(`${baseURL}/api/auth/sign-in/credentials`, { + const response = await fetch(`${API_BASE_URL}/auth/sign-in/credentials`, { method: "POST", headers: { "Content-Type": "application/json",