first commit
This commit is contained in:
177
react/client.ts
Normal file
177
react/client.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user