fix(#410): align BetterAuth basePath and auth client with NestJS routing
All checks were successful
ci/woodpecker/push/web Pipeline was successful
ci/woodpecker/push/api Pipeline was successful

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 19:41:08 -06:00
parent 31ce9e920c
commit 444fa1116a
4 changed files with 27 additions and 33 deletions

View File

@@ -83,6 +83,7 @@ export function createAuth(prisma: PrismaClient) {
validateOidcConfig(); validateOidcConfig();
return betterAuth({ return betterAuth({
basePath: "/auth",
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {
provider: "postgresql", provider: "postgresql",
}), }),

View File

@@ -3,20 +3,19 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { LoginButton } from "./LoginButton"; import { LoginButton } from "./LoginButton";
// Mock window.location const { mockOAuth2 } = vi.hoisted(() => ({
const mockLocation = { mockOAuth2: vi.fn(),
href: "", }));
assign: vi.fn(),
}; vi.mock("@/lib/auth-client", () => ({
Object.defineProperty(window, "location", { signIn: {
value: mockLocation, oauth2: mockOAuth2,
writable: true, },
}); }));
describe("LoginButton", (): void => { describe("LoginButton", (): void => {
beforeEach((): void => { beforeEach((): void => {
mockLocation.href = ""; mockOAuth2.mockClear();
mockLocation.assign.mockClear();
}); });
it("should render sign in button", (): void => { it("should render sign in button", (): void => {
@@ -25,14 +24,17 @@ describe("LoginButton", (): void => {
expect(button).toBeInTheDocument(); expect(button).toBeInTheDocument();
}); });
it("should redirect to OIDC endpoint on click", async (): Promise<void> => { it("should initiate OAuth2 sign-in on click", async (): Promise<void> => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<LoginButton />); render(<LoginButton />);
const button = screen.getByRole("button", { name: /sign in/i }); const button = screen.getByRole("button", { name: /sign in/i });
await user.click(button); 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 => { it("should have proper styling", (): void => {

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { Button } from "@mosaic/ui"; import { Button } from "@mosaic/ui";
import { API_BASE_URL } from "@/lib/config"; import { signIn } from "@/lib/auth-client";
export function LoginButton(): React.JSX.Element { export function LoginButton(): React.JSX.Element {
const handleLogin = (): void => { const handleLogin = (): void => {
// Redirect to the backend OIDC authentication endpoint // Use BetterAuth's genericOAuth client to initiate the OIDC flow.
// BetterAuth will handle the OIDC flow and redirect back to the callback // This POSTs to /auth/sign-in/oauth2 and follows the returned redirect URL.
window.location.assign(`${API_BASE_URL}/auth/signin/authentik`); void signIn.oauth2({ providerId: "authentik", callbackURL: "/" });
}; };
return ( return (

View File

@@ -7,20 +7,16 @@
* - Automatic token refresh * - Automatic token refresh
*/ */
import { createAuthClient } from "better-auth/react"; 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({ export const authClient = createAuthClient({
// Base URL for auth API baseURL: API_BASE_URL,
baseURL: basePath: "/auth",
typeof window !== "undefined" plugins: [genericOAuthClient()],
? window.location.origin
: (process.env.BETTER_AUTH_URL ?? "http://localhost:3042"),
// Plugins can be added here when needed
plugins: [],
}); });
/** /**
@@ -36,12 +32,7 @@ export const { signIn, signOut, useSession, getSession } = authClient;
* and the default BetterAuth client expects email. * and the default BetterAuth client expects email.
*/ */
export async function signInWithCredentials(username: string, password: string): Promise<unknown> { export async function signInWithCredentials(username: string, password: string): Promise<unknown> {
const baseURL = const response = await fetch(`${API_BASE_URL}/auth/sign-in/credentials`, {
typeof window !== "undefined"
? window.location.origin
: (process.env.BETTER_AUTH_URL ?? "http://localhost:3042");
const response = await fetch(`${baseURL}/api/auth/sign-in/credentials`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",