313 lines
8.5 KiB
JavaScript
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}`;
|
|
}
|