panels-origin/web-panel/composables/useApi.ts
Cursor Agent 1f85b44e80
fix(web-panel): unwrap API envelope post-login and improve error screen
- Normalize RPC responses in useApi so legacy pages receive flat shapes
- Harden dashboard index.vue against undefined arrays and load failures
- Redesign Nuxt error boundary with Oops message, error code, and retry

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-04 06:56:25 +00:00

77 lines
2.5 KiB
TypeScript

import { displayErrorMessage, isApiEnvelope, unwrapApiPayload } from "./api-response.ts";
export function useApi() {
async function api<T>(path: string, opts: RequestInit & { json?: unknown } = {}): Promise<T> {
const headers = new Headers(opts.headers);
if (opts.json !== undefined) headers.set("Content-Type", "application/json");
const res = await fetch(path, {
...opts,
headers,
credentials: "include",
body: opts.json !== undefined ? JSON.stringify(opts.json) : opts.body,
});
const ct = res.headers.get("content-type") || "";
const isJson = ct.includes("application/json");
const raw = isJson ? await res.json().catch(() => ({})) : null;
if (!res.ok) {
const errBody = isApiEnvelope(raw) ? raw : (raw as { error?: string } | null);
throw Object.assign(
new Error(displayErrorMessage(errBody, res.statusText)),
{ status: res.status, data: errBody },
);
}
if (isJson) return unwrapApiPayload<T>(raw);
return (await res.blob()) as T;
}
async function download(path: string, filename: string) {
const blob = await api<Blob>(path);
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
window.setTimeout(() => URL.revokeObjectURL(url), 2000);
}
return { api, download };
}
export const useAuth = () => {
const user = useState<{
id: number;
username: string;
display_name: string;
company_id?: number | null;
company_code?: string | null;
company_name?: string | null;
company_kind?: string | null;
tenant_id?: number | null;
role?: "platform_admin" | "tenant_admin" | "user";
realm?: "app" | "platform";
must_change_password?: boolean;
} | null>("auth-user", () => null);
const { api } = useApi();
async function fetchMe() {
try {
const r = await api<{ user: NonNullable<typeof user.value> }>("/v1/auth/me");
user.value = r.user;
} catch {
user.value = null;
}
}
async function login(username: string, password: string) {
const r = await api<{ user: NonNullable<typeof user.value> }>("/v1/auth/login", {
method: "POST",
json: { username, password },
});
user.value = r.user;
}
async function logout() {
await api("/v1/auth/logout", { method: "POST" });
user.value = null;
await navigateTo("/login");
}
return { user, fetchMe, login, logout };
};