178 lines
5.4 KiB
TypeScript
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();
|
|
}
|
|
};
|
|
}
|