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();
return betterAuth({
basePath: "/auth",
database: prismaAdapter(prisma, {
provider: "postgresql",
}),

View File

@@ -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<void> => {
it("should initiate OAuth2 sign-in on click", async (): Promise<void> => {
const user = userEvent.setup();
render(<LoginButton />);
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 => {

View File

@@ -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 (

View File

@@ -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<unknown> {
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",