import axios, { AxiosError } from "axios"; import { inferRoleFromEmail, normalizeRole, type UserRole } from "@/lib/auth"; import { backendEndpointDocs } from "@/services/backendEndpoints"; type TokenGetter = () => string | null; export type UserAccountStatus = "active" | "inactive"; export type LoginResponse = { token: string; user: { id?: number; name?: string; email: string; role?: UserRole | string; status?: UserAccountStatus | string; }; }; export type UserProfile = { id: number; name: string; email: string; role?: UserRole | string; status?: UserAccountStatus | string; }; export type RegisterPayload = { fullName: string; employeeId: string; email: string; departmentBranch: string; phone: string; password: string; role: BackendRegisterRole; }; export type RegisterResponse = { message: string; }; export type BackendRegisterRole = "admin" | "student" | "lecturer"; let getAuthToken: TokenGetter = () => null; let runtimeAuthToken: string | null = null; export function bindTokenGetter(getter: TokenGetter) { getAuthToken = getter; } export function setRuntimeAuthToken(token: string | null) { runtimeAuthToken = token; } function resolveAuthToken() { return runtimeAuthToken ?? getAuthToken(); } function normalizeError(error: unknown): Error { if (error instanceof Error) return error; return new Error("Unknown API error"); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function readString(source: Record, keys: string[]) { for (const key of keys) { const value = source[key]; if (typeof value === "string" && value.trim().length > 0) return value; } return undefined; } function resolvePayloadRoot(payload: unknown) { if (!isRecord(payload)) return {}; const data = payload.data; if (isRecord(data)) return data; return payload; } function normalizeUserStatus(value: unknown): UserAccountStatus | undefined { if (typeof value !== "string") return undefined; const lowered = value.trim().toLowerCase(); if (lowered === "active" || lowered === "inactive") return lowered; return undefined; } function toLoginResponse( payload: unknown, email: string, role?: UserRole, ): LoginResponse { const root = resolvePayloadRoot(payload); const token = readString(root, ["token", "access_token", "accessToken"]) ?? readString(isRecord(payload) ? payload : {}, [ "token", "access_token", "accessToken", ]); const userSource = isRecord(root.user) ? root.user : isRecord(payload) && isRecord(payload.user) ? payload.user : {}; const resolvedEmail = readString(userSource, ["email"]) ?? email; const resolvedRole = normalizeRole( readString(userSource, ["role"]) ?? role ?? inferRoleFromEmail(resolvedEmail), ); const resolvedStatus = normalizeUserStatus(userSource.status); if (resolvedStatus === "inactive") { throw new Error( "Akun belum diverifikasi. Silakan verifikasi email terlebih dahulu.", ); } if (!token) { throw new Error("Auth token missing from sign-in response"); } return { token, user: { id: typeof userSource.id === "number" ? userSource.id : undefined, name: readString(userSource, ["name"]), email: resolvedEmail, role: resolvedRole, status: resolvedStatus, }, }; } function toUserProfile(payload: unknown): UserProfile { const root = resolvePayloadRoot(payload); const fallback = isRecord(payload) ? payload : {}; const idValue = root.id ?? fallback.id; const id = typeof idValue === "number" ? idValue : typeof idValue === "string" && Number.isFinite(Number(idValue)) ? Number(idValue) : 0; const email = readString(root, ["email"]) ?? readString(fallback, ["email"]) ?? "unknown@example.com"; return { id, name: readString(root, ["name"]) ?? readString(fallback, ["name"]) ?? "Unknown User", email, role: readString(root, ["role"]) ?? readString(fallback, ["role"]) ?? inferRoleFromEmail(email), status: normalizeUserStatus(root.status) ?? normalizeUserStatus(fallback.status), }; } function toRegisterMessage(payload: unknown): string { const root = resolvePayloadRoot(payload); return ( readString(root, ["message", "detail", "status"]) ?? (isRecord(payload) ? readString(payload, ["message", "detail", "status"]) : undefined) ?? "Registrasi berhasil. Cek email untuk verifikasi akun." ); } function isDummyAuthEnabled() { const mode = import.meta.env.VITE_DUMMY_AUTH_MODE?.toLowerCase(); if (mode === "on" || mode === "true" || mode === "1") return true; if (mode === "off" || mode === "false" || mode === "0") return false; return import.meta.env.DEV; } function shouldUseDummyFallback(error: unknown) { if (!isDummyAuthEnabled()) return false; if (!(error instanceof Error)) return false; const message = error.message.toLowerCase(); return ( message.includes("status code 404") || message.includes("network error") || message.includes("failed to fetch") || message.includes("ecconnrefused") ); } function toText(value: unknown): string | undefined { if (typeof value === "string") { const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } if (typeof value === "number" || typeof value === "bigint") { return String(value); } return undefined; } function extractApiErrorMessage(payload: unknown): string | undefined { const directText = toText(payload); if (directText) return directText; if (!isRecord(payload)) return undefined; const direct = readString(payload, ["message", "detail", "error", "status"]) ?? undefined; if (direct) return direct; const detail = payload.detail; if (Array.isArray(detail)) { const parts = detail .map((item) => { if (typeof item === "string") return item; if (!isRecord(item)) return undefined; const msg = toText(item.msg) ?? toText(item.message); const loc = Array.isArray(item.loc) && item.loc.length > 0 ? item.loc .map((part) => toText(part) ?? "") .filter(Boolean) .join(".") : undefined; if (loc && msg) return `${loc}: ${msg}`; return msg; }) .filter((item): item is string => Boolean(item)); if (parts.length > 0) return parts.join("; "); } return undefined; } function resolveAuthEndpoint(kind: "signIn" | "signUp" | "me") { return backendEndpointDocs.auth[kind]; } export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL ?? "/api/v1", headers: { Accept: "application/json", }, }); api.interceptors.request.use((config) => { const token = resolveAuthToken(); if (!token) return config; config.headers = config.headers ?? {}; config.headers.Authorization = `Bearer ${token}`; return config; }); api.interceptors.response.use( (response) => response, (error: AxiosError<{ message?: string }>) => { const detailMessage = extractApiErrorMessage(error.response?.data); const backendMessage = detailMessage ?? error.response?.data?.message ?? error.message ?? "Request failed unexpectedly"; return Promise.reject(new Error(backendMessage)); }, ); export async function login(email: string, password: string, role?: UserRole) { try { const response = await api.post( resolveAuthEndpoint("signIn"), { email, password, }, ); return toLoginResponse(response.data, email, role); } catch (error) { if (shouldUseDummyFallback(error)) { const resolvedRole = normalizeRole( role ?? inferRoleFromEmail(email), ); return { token: `dummy-token-${Date.now()}`, user: { id: 1, name: "Dummy User", email, role: resolvedRole, }, }; } throw error; } } export async function getProfile() { const response = await api.get(resolveAuthEndpoint("me")); return toUserProfile(response.data); } export async function registerAccount( payload: RegisterPayload, ): Promise { const body = { name: payload.fullName, email: payload.email, password: payload.password, identity_number: payload.employeeId, role: payload.role, }; const response = await api.post( resolveAuthEndpoint("signUp"), body, ); return { message: toRegisterMessage(response.data) }; } export async function checkHealth() { try { const response = await api.get<{ status: string; service: string }>( "/health", ); return response.data; } catch (error) { throw normalizeError(error); } }