gh_UserManager/apps/web/lib/api.ts
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

169 lines
4.5 KiB
TypeScript

// Central API client for the identity platform web app.
// The auth lifecycle (login/register/refresh/logout) is handled by the Next.js
// Route Handlers under app/api/auth/*, which set HttpOnly cookies so the
// server-side middleware can guard routes. Data calls attach a Bearer token
// from localStorage (mirroring Hamsoo's apiclient token-storage pattern).
export const ACCESS_TOKEN_KEY = "access_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const SESSION_ID_KEY = "session_id";
// Same-origin proxy base for auth lifecycle calls (sets HttpOnly cookies).
const AUTH_PROXY = "/api/auth";
export function getAccessToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
export function getRefreshToken(): string | null {
if (typeof window === "undefined") return null;
return localStorage.getItem(REFRESH_TOKEN_KEY);
}
export function setTokens(
access: string,
refresh: string,
sessionId?: string,
): void {
if (typeof window === "undefined") return;
localStorage.setItem(ACCESS_TOKEN_KEY, access);
localStorage.setItem(REFRESH_TOKEN_KEY, refresh);
if (sessionId) localStorage.setItem(SESSION_ID_KEY, sessionId);
}
export function clearTokens(): void {
if (typeof window === "undefined") return;
localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(SESSION_ID_KEY);
}
function apiBase(): string {
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1";
}
export interface ApiResult {
ok: boolean;
status: number;
data: any;
}
export async function authLogin(
email: string,
password: string,
mfaCode?: string,
): Promise<ApiResult> {
const res = await fetch(`${AUTH_PROXY}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email,
password,
mfa_code: mfaCode || "",
}),
});
const data = await res.json().catch(() => ({}));
return { ok: res.ok, status: res.status, data };
}
export async function authRegister(payload: {
email: string;
password: string;
password_confirm: string;
full_name?: string;
username?: string;
}): Promise<ApiResult> {
const res = await fetch(`${AUTH_PROXY}/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => ({}));
return { ok: res.ok, status: res.status, data };
}
export async function authLogout(refresh?: string | null): Promise<boolean> {
try {
const res = await fetch(`${AUTH_PROXY}/logout`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
refresh: refresh || getRefreshToken() || "",
logout_type: "global",
}),
});
return res.ok;
} catch {
return false;
}
}
async function tryRefresh(): Promise<string | null> {
const refresh = getRefreshToken();
if (!refresh) {
clearTokens();
return null;
}
try {
const res = await fetch(`${AUTH_PROXY}/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh }),
});
if (!res.ok) {
clearTokens();
return null;
}
const data = await res.json();
const access = data.access;
if (!access) return null;
setTokens(access, data.refresh ?? refresh, data.session_id);
return access;
} catch {
clearTokens();
return null;
}
}
export async function apiFetch(
url: string,
options: RequestInit = {},
): Promise<Response> {
const fullUrl = url.startsWith("http") ? url : `${apiBase()}${url}`;
const token = getAccessToken();
const buildHeaders = (authToken?: string): Headers => {
const headers = new Headers(options.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
if (authToken) headers.set("Authorization", `Bearer ${authToken}`);
return headers;
};
let res = await fetch(fullUrl, {
...options,
headers: buildHeaders(token || undefined),
});
// Automatic refresh on 401 (single retry).
if (res.status === 401) {
const newToken = await tryRefresh();
if (newToken) {
res = await fetch(fullUrl, {
...options,
headers: buildHeaders(newToken),
});
}
}
return res;
}
export async function fetchMe(): Promise<any | null> {
const res = await apiFetch("/auth/me/");
if (!res.ok) return null;
return res.json();
}