93 lines
2.9 KiB
TypeScript
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);
|
|
}
|