Files
lib-auth/react/client.ts
2026-04-01 15:17:46 +02:00

178 lines
5.4 KiB
TypeScript

import type { AuthProviderAvailability, PasswordResetMode } from "./types";
type CreateAuthClientOptions = {
apiUrl: (path: string) => string;
authUrl?: (path: string) => string;
fetchImpl?: typeof fetch;
credentials?: RequestCredentials;
defaultOAuthCallbackUrl?: string | (() => string);
};
type LoginInput = {
email: string;
password: string;
};
type RegisterInput = LoginInput & {
name: string;
};
type PasswordResetValidationPayload = {
email?: string;
mode?: PasswordResetMode;
error?: string;
};
type JsonErrorPayload = {
error?: string;
};
async function readJsonError(response: Response, fallback: string): Promise<Error> {
const payload = (await response.json().catch(() => null)) as JsonErrorPayload | null;
return new Error(payload?.error ?? fallback);
}
export function createAuthClient(options: CreateAuthClientOptions) {
const fetchImpl = options.fetchImpl ?? fetch;
const authUrl = options.authUrl ?? options.apiUrl;
const credentials = options.credentials ?? "include";
function resolveDefaultOAuthCallbackUrl(): string {
const configured = options.defaultOAuthCallbackUrl;
if (typeof configured === "function") {
return configured();
}
if (typeof configured === "string" && configured.trim().length > 0) {
return configured;
}
return `${window.location.origin}/chat`;
}
async function request(path: string, init?: RequestInit): Promise<Response> {
return fetchImpl(options.apiUrl(path), {
...init,
credentials,
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {})
}
});
}
return {
async getProviders(): Promise<AuthProviderAvailability> {
const response = await request("/api/auth/providers");
if (!response.ok) {
throw await readJsonError(response, "providers_unavailable");
}
return (await response.json()) as AuthProviderAvailability;
},
async getCurrentUser<TUser>(): Promise<TUser> {
const response = await request("/api/me");
if (!response.ok) {
throw await readJsonError(response, "Unauthorized");
}
const payload = (await response.json()) as { user: TUser };
return payload.user;
},
async register(input: RegisterInput): Promise<void> {
const response = await request("/api/auth/register", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Registration failed");
}
},
async login(input: LoginInput): Promise<void> {
const response = await request("/api/auth/login", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Sign in failed");
}
},
async requestPasswordReset(email: string): Promise<void> {
const response = await request("/api/auth/password-reset/request", {
method: "POST",
body: JSON.stringify({ email })
});
if (!response.ok) {
throw await readJsonError(response, "Password reset request failed");
}
},
async validatePasswordResetToken(token: string): Promise<{ email: string; mode: PasswordResetMode }> {
const response = await request(`/api/auth/password-reset/validate?token=${encodeURIComponent(token)}`, {
headers: {}
});
const payload = (await response.json().catch(() => null)) as PasswordResetValidationPayload | null;
if (!response.ok || !payload?.email || (payload.mode !== "reset" && payload.mode !== "create")) {
throw new Error(payload?.error ?? "Invalid reset link");
}
return {
email: payload.email,
mode: payload.mode
};
},
async confirmPasswordReset(input: { token: string; password: string }): Promise<void> {
const response = await request("/api/auth/password-reset/confirm", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Invalid reset link");
}
},
async logout(): Promise<void> {
const response = await request("/api/auth/logout", {
method: "POST"
});
if (!response.ok) {
throw await readJsonError(response, "Logout failed");
}
},
async startOAuthSignIn(provider: string, callbackUrl = resolveDefaultOAuthCallbackUrl()): Promise<void> {
const response = await fetchImpl(authUrl("/auth/csrf"), {
credentials
});
if (!response.ok) {
throw await readJsonError(response, "Sign in failed");
}
const payload = (await response.json()) as { csrfToken?: string };
if (!payload.csrfToken) {
throw new Error("Sign in failed");
}
const form = document.createElement("form");
form.method = "POST";
form.action = authUrl(`/auth/signin/${provider}`);
form.style.display = "none";
const csrfInput = document.createElement("input");
csrfInput.type = "hidden";
csrfInput.name = "csrfToken";
csrfInput.value = payload.csrfToken;
form.appendChild(csrfInput);
const callbackInput = document.createElement("input");
callbackInput.type = "hidden";
callbackInput.name = "callbackUrl";
callbackInput.value = callbackUrl;
form.appendChild(callbackInput);
document.body.appendChild(form);
form.submit();
}
};
}