lifedebugger's picture
Deploy files from GitHub repository with LFS
bb09403
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<string, unknown> {
return typeof value === "object" && value !== null;
}
function readString(source: Record<string, unknown>, 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<unknown>(
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<unknown>(resolveAuthEndpoint("me"));
return toUserProfile(response.data);
}
export async function registerAccount(
payload: RegisterPayload,
): Promise<RegisterResponse> {
const body = {
name: payload.fullName,
email: payload.email,
password: payload.password,
identity_number: payload.employeeId,
role: payload.role,
};
const response = await api.post<unknown>(
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);
}
}