ForgeFlow-ERP/frontend/src/services/api.js
2026-06-12 16:29:05 +08:00

313 lines
8.5 KiB
JavaScript

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000/api";
const SESSION_KEY = "forgeflow.erp.session";
async function parseResponse(response) {
const contentType = response.headers.get("content-type") || "";
const isJson = contentType.includes("application/json");
const data = isJson ? await response.json() : await response.text();
if (!response.ok) {
const detail = typeof data === "object" && data?.detail ? data.detail : `Request failed: ${response.status}`;
throw new Error(detail);
}
return data;
}
function isSessionExpired(session) {
if (!session?.access_token || !session?.expires_at) {
return true;
}
const expiresAt = Date.parse(session.expires_at);
if (Number.isNaN(expiresAt)) {
return true;
}
return expiresAt <= Date.now();
}
export async function requestJson(path, options = {}) {
const { skipAuthRedirect = false, headers = {}, ...fetchOptions } = options;
const session = readSession();
const authHeaders =
session?.access_token && !isSessionExpired(session)
? { Authorization: `Bearer ${session.access_token}` }
: {};
const response = await fetch(`${API_BASE_URL}${path}`, {
headers: {
"Content-Type": "application/json",
...authHeaders,
...headers
},
...fetchOptions
});
if ((response.status === 401 || response.status === 403) && !skipAuthRedirect) {
clearSession();
if (typeof window !== "undefined" && window.location.pathname !== "/login") {
window.location.href = "/login";
}
}
return parseResponse(response);
}
export async function fetchResource(path, fallbackData) {
try {
return await requestJson(path, { method: "GET" });
} catch (error) {
return fallbackData;
}
}
export async function postResource(path, payload) {
return requestJson(path, {
method: "POST",
body: JSON.stringify(payload)
});
}
export async function uploadResource(path, formData) {
const session = readSession();
const authHeaders =
session?.access_token && !isSessionExpired(session)
? { Authorization: `Bearer ${session.access_token}` }
: {};
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "POST",
headers: authHeaders,
body: formData
});
if ((response.status === 401 || response.status === 403) && typeof window !== "undefined" && window.location.pathname !== "/login") {
clearSession();
window.location.href = "/login";
}
return parseResponse(response);
}
function getDownloadFilename(response, fallbackFilename) {
const disposition = response.headers.get("content-disposition") || "";
const encodedMatch = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (encodedMatch?.[1]) {
try {
return decodeURIComponent(encodedMatch[1]);
} catch (error) {
return fallbackFilename;
}
}
const plainMatch = disposition.match(/filename="?([^";]+)"?/i);
return plainMatch?.[1] || fallbackFilename;
}
export async function downloadResource(path, fallbackFilename = "download.xlsx") {
const session = readSession();
const authHeaders =
session?.access_token && !isSessionExpired(session)
? { Authorization: `Bearer ${session.access_token}` }
: {};
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "GET",
headers: authHeaders
});
if ((response.status === 401 || response.status === 403) && typeof window !== "undefined" && window.location.pathname !== "/login") {
clearSession();
window.location.href = "/login";
}
if (!response.ok) {
return parseResponse(response);
}
const blob = await response.blob();
const filename = getDownloadFilename(response, fallbackFilename);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
return { filename };
}
export async function postDownloadResource(path, payload, fallbackFilename = "download.zip") {
const session = readSession();
const authHeaders =
session?.access_token && !isSessionExpired(session)
? { Authorization: `Bearer ${session.access_token}` }
: {};
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...authHeaders
},
body: JSON.stringify(payload)
});
if ((response.status === 401 || response.status === 403) && typeof window !== "undefined" && window.location.pathname !== "/login") {
clearSession();
window.location.href = "/login";
}
if (!response.ok) {
return parseResponse(response);
}
const blob = await response.blob();
const filename = getDownloadFilename(response, fallbackFilename);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
return { filename };
}
export async function openResource(path) {
const session = readSession();
const authHeaders =
session?.access_token && !isSessionExpired(session)
? { Authorization: `Bearer ${session.access_token}` }
: {};
const response = await fetch(`${API_BASE_URL}${path}`, {
method: "GET",
headers: authHeaders
});
if ((response.status === 401 || response.status === 403) && typeof window !== "undefined" && window.location.pathname !== "/login") {
clearSession();
window.location.href = "/login";
}
if (!response.ok) {
return parseResponse(response);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
window.open(url, "_blank", "noopener,noreferrer");
window.setTimeout(() => window.URL.revokeObjectURL(url), 60_000);
return { opened: true };
}
export async function putResource(path, payload) {
return requestJson(path, {
method: "PUT",
body: JSON.stringify(payload)
});
}
export async function deleteResource(path) {
return requestJson(path, {
method: "DELETE"
});
}
export function saveSession(user) {
localStorage.setItem(SESSION_KEY, JSON.stringify(user));
}
export function readSession() {
const raw = localStorage.getItem(SESSION_KEY);
if (!raw) {
return null;
}
try {
const session = JSON.parse(raw);
if (isSessionExpired(session)) {
localStorage.removeItem(SESSION_KEY);
return null;
}
return session;
} catch (error) {
console.warn("Failed to parse session", error);
localStorage.removeItem(SESSION_KEY);
return null;
}
}
export function clearSession() {
localStorage.removeItem(SESSION_KEY);
}
export function isAuthenticated() {
return Boolean(readSession());
}
function pemToArrayBuffer(pem) {
const base64Text = pem
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
.replace(/-----END PUBLIC KEY-----/g, "")
.replace(/\s/g, "");
const binary = window.atob(base64Text);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes.buffer;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let index = 0; index < bytes.byteLength; index += 1) {
binary += String.fromCharCode(bytes[index]);
}
return window.btoa(binary);
}
async function encryptLoginPassword(password, publicKeyPem) {
if (!window.crypto?.subtle) {
throw new Error("当前访问方式不支持安全加密登录,请使用 HTTPS 或 localhost 地址访问系统");
}
const publicKey = await window.crypto.subtle.importKey(
"spki",
pemToArrayBuffer(publicKeyPem),
{
name: "RSA-OAEP",
hash: "SHA-256"
},
false,
["encrypt"]
);
const ciphertext = await window.crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
publicKey,
new TextEncoder().encode(password)
);
return arrayBufferToBase64(ciphertext);
}
export async function login(username, password) {
const loginKey = await requestJson("/auth/login-key", {
method: "GET",
skipAuthRedirect: true
});
const passwordCiphertext = await encryptLoginPassword(password, loginKey.public_key_pem);
return requestJson("/auth/login", {
method: "POST",
body: JSON.stringify({
username,
password_ciphertext: passwordCiphertext,
login_key_id: loginKey.key_id
}),
skipAuthRedirect: true
});
}
export function buildApiUrl(path) {
return `${API_BASE_URL}${path}`;
}