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 <noreply@anthropic.com>
This commit is contained in:
@@ -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",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user