Files
Cibello-app/apps/admin/src/api.ts
T
2026-08-05 19:21:11 +07:00

93 lines
2.9 KiB
TypeScript

import brand from "../../../brand.config.json";
export const BRAND = brand as { name: string; slug: string };
const ADMIN_TOKEN_KEY = `${BRAND.slug}_admin_token`;
/** Enkel API-klient för adminpanelen. Token hålls i minne + sessionStorage. */
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:4000";
let accessToken: string | null = sessionStorage.getItem(ADMIN_TOKEN_KEY);
export function setToken(token: string | null): void {
accessToken = token;
if (token) sessionStorage.setItem(ADMIN_TOKEN_KEY, token);
else sessionStorage.removeItem(ADMIN_TOKEN_KEY);
}
export function hasToken(): boolean {
return accessToken != null;
}
export class ApiRequestError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
message: string,
) {
super(message);
}
}
export async function api<T = unknown>(
path: string,
options: { method?: string; body?: unknown } = {},
): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: options.method ?? "GET",
headers: {
"content-type": "application/json",
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
},
body: options.body != null ? JSON.stringify(options.body) : undefined,
});
const json = (await res.json().catch(() => ({}))) as {
error?: { code?: string; message?: string };
} & T;
if (!res.ok) {
if (res.status === 401) setToken(null);
throw new ApiRequestError(
res.status,
json.error?.code ?? "UNKNOWN",
json.error?.message ?? `HTTP ${res.status}`,
);
}
return json as T;
}
export interface LoginResult {
totpRequired: boolean;
preAuthToken?: string;
}
export async function login(email: string, password: string): Promise<LoginResult> {
const result = await api<{
accessToken?: string;
user?: { role: string };
totpRequired?: boolean;
preAuthToken?: string;
}>("/v1/auth/login", { method: "POST", body: { email, password } });
if (result.totpRequired && result.preAuthToken) {
return { totpRequired: true, preAuthToken: result.preAuthToken };
}
if (!result.user || !result.accessToken) {
throw new ApiRequestError(500, "INTERNAL", "Oväntat svar från inloggningen.");
}
if (result.user.role !== "admin" && result.user.role !== "moderator") {
throw new ApiRequestError(403, "FORBIDDEN", "Kontot saknar admin-behörighet.");
}
setToken(result.accessToken);
return { totpRequired: false };
}
/** Steg 2 av inloggningen: engångskoden från autentiseringsappen. */
export async function verifyTotp(preAuthToken: string, code: string): Promise<void> {
const result = await api<{ accessToken: string; user: { role: string } }>(
"/v1/auth/totp-verify",
{ method: "POST", body: { preAuthToken, code } },
);
if (result.user.role !== "admin" && result.user.role !== "moderator") {
throw new ApiRequestError(403, "FORBIDDEN", "Kontot saknar admin-behörighet.");
}
setToken(result.accessToken);
}