Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user