Files
console-dev/app/lib/auth.js
T
2026-11-06 00:04:38 +08:00

857 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import CryptoJS from "crypto-js";
import { defineStore } from "pinia";
import { navigateTo, useRuntimeConfig } from "#app";
import { computed, ref } from "vue";
var ce = [
[/产品类型错误.{0,30}安全组[^\n]*/g, `该商品未接入安全组`],
[/商品未关联.{0,20}模块接口/g, `该商品暂不可用`],
[/未关联.{0,12}模块接口/g, `该商品暂不可用`],
[/尚未接入.{0,12}模块/g, `该商品暂不可用`],
];
var le = [
[/快快云-弹性云|弹性云/g, ``],
[/快快云|快快/g, ``],
[/魔方云|魔方财务|智简魔方|智简/g, ``],
[/美得云|美德云|meidecloud/gi, ``],
[/dwvps[a-z0-9_]*/gi, ``],
[/miloce[a-z0-9_]*/gi, ``],
[/jinghanyun[a-z0-9_]*/gi, ``],
[/kkidc[a-z0-9_]*/gi, ``],
[/kky[a-z0-9_]*/gi, ``],
[/idcsmart[a-z0-9_]*/gi, ``],
[/mf[_-]?cloud[a-z0-9_]*/gi, ``],
[/mf[_-]?dcim[a-z0-9_]*/gi, ``],
[/common[_-]?cloud[a-z0-9_]*/gi, ``],
[/pve[_-]?cloud[a-z0-9_]*/gi, ``],
];
var ue =
/模块接口|自定义配置|模块未关联|模块标识|上游|第三方云|[a-z]{2,}[_-][a-z0-9_]+|\bDCIM\b|whmcs|reserver|https?:\/\//i;
function W(e) {
let t = typeof e == `string` ? e : ``;
if (!t) return ``;
let n = t;
for (let [e, t] of ce) n = n.replace(e, t);
for (let [e, t] of le) n = n.replace(e, t);
return (
(n = n
.replace(/\s{2,}/g, ` `)
.replace(/[,,、]{2,}/g, `,`)
.replace(/^[\s,,、::;;-]+|[\s,,、::;;-]+$/g, ``)
.trim()),
n.length < 2 || ue.test(n) ? `操作失败,请稍后重试或联系客服` : n
);
}
function de(e) {
let t = typeof e == `string` ? e : ``;
return /^\s*上游熔断解除[::]/u.test(t)
? `服务连接已恢复正常`
: /^\s*上游熔断[::]/u.test(t)
? `服务连接暂时异常,请稍后重试`
: t;
}
var fe = 32;
var pe = 200;
var me = 6e4;
var G = new Map();
var he = Symbol(`uninitialized`);
var ge = 0;
function _e() {
(ge++, G.clear());
}
function ve() {
return ge;
}
function ye(e, t, n = ``) {
return JSON.stringify([e, n, t], (e, t) =>
t && typeof t == `object` && !Array.isArray(t)
? Object.fromEntries(
Object.keys(t)
.sort()
.map((e) => [e, t[e]]),
)
: t,
);
}
function be(e) {
return JSON.parse(JSON.stringify(e));
}
function xe(e, t, n = Date.now()) {
he !== e && (_e(), (he = e));
for (let [e, t] of G) t.expires <= n && G.delete(e);
let r = G.get(t);
if (r) return (G.delete(t), G.set(t, r), be(r.data));
}
function Se(e, t, n, r, i = Date.now()) {
if (e === he && r === ge) {
if (!Array.isArray(n.list) || n.list.length > pe) {
G.delete(t);
return;
}
try {
let e = be(n);
(G.delete(t),
G.size >= fe && G.delete(G.keys().next().value),
G.set(t, {
data: e,
expires: i + me,
}));
} catch {
G.delete(t);
}
}
}
var Ce = 2147483647;
function K(e, t) {
let n =
typeof e == `number`
? e
: typeof e == `string` && /^\d+$/.test(e)
? Number(e)
: NaN;
if (!Number.isSafeInteger(n) || n < t || n > Ce)
throw Error(`购买信息无效,请重新确认后重试`);
return n;
}
function we(e) {
if (!e || typeof e != `object` || Array.isArray(e))
throw Error(`购买信息无效,请重新确认后重试`);
return e;
}
function Te(e) {
let t = we(e);
return {
product_id: K(t.product_id, 1),
qty: K(t.qty, 1),
};
}
function Ee(e, t, n) {
if (e === `POST` && t === `/product/settle`)
return {
mode: `single`,
...Te(n),
};
if (e === `POST` && t === `/cart`) {
let e = we(n);
if (e.products !== void 0) {
if (
!Array.isArray(e.products) ||
!e.products.length ||
e.products.length > 1e3
)
throw Error(`购买信息无效,请重新确认后重试`);
return {
mode: `cart_add`,
products: e.products.map(Te),
};
}
return {
mode: `cart_add`,
...Te(e),
};
}
if (e === `POST` && t === `/cart/settle`) {
let e = we(n);
if (!Array.isArray(e.positions) || !e.positions.length)
throw Error(`请选择要结算的商品`);
return {
mode: `cart_settle`,
positions: [...new Set(e.positions.map((e) => K(e, 0)))],
};
}
let r = e === `PUT` ? /^\/cart\/(\d+)(\/qty)?$/.exec(t) : null;
if (r) {
let e = we(n);
return {
mode: `cart_update`,
position: K(r[1], 0),
qty: K(e.qty, 1),
...(r[2]
? {}
: {
product_id: K(e.product_id, 1),
}),
};
}
return null;
}
function De(e) {
if (!e || typeof e != `object` || Array.isArray(e)) return !1;
let t = e;
return (
typeof t.allowed == `boolean` &&
typeof t.message == `string` &&
(t.allowed || !!t.message.trim())
);
}
class ApiError extends Error {
status;
data;
constructor(status, message, data) {
super(message);
this.status = status;
this.data = data;
this.name = "ApiError";
}
}
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
const FILE_TRANSFER_TIMEOUT_MS = 120_000;
function requestTimeoutError() {
return new ApiError(408, "请求超时,请稍后重试");
}
async function withRequestDeadline(
operation,
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
controller = new AbortController(),
) {
const duration =
Number.isFinite(timeoutMs) && timeoutMs > 0 && timeoutMs <= 2_147_483_647
? timeoutMs
: DEFAULT_REQUEST_TIMEOUT_MS;
let timer;
const deadline = new Promise((_, reject) => {
timer = setTimeout(() => {
controller.abort();
reject(requestTimeoutError());
}, duration);
});
try {
// The deadline also covers response-body decoding and any purchase preflight.
return await Promise.race([
Promise.resolve().then(() => operation(controller.signal)),
deadline,
]);
} finally {
clearTimeout(timer);
}
}
function assertRequestActive(signal) {
if (signal.aborted) throw requestTimeoutError();
}
function normalizeRequestError(error) {
return error instanceof ApiError
? error
: new ApiError(0, "网络异常,请检查网络后重试");
}
function useApi() {
const config = useRuntimeConfig();
const auth = useAuth();
function clearUnauthorizedSession(requestToken) {
if (
!requestToken ||
localStorage.getItem("jwt") !== requestToken ||
auth.token !== requestToken
)
return;
auth.clear();
if (window.location.pathname === "/login") return;
const redirect = window.location.pathname + window.location.search;
try {
void Promise.resolve(
navigateTo({
path: "/login",
query: redirect === "/" ? undefined : { redirect },
}),
).catch(() => {});
} catch {}
}
function responseObject(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
function responseError(response, payload, requestToken, fallbackMessage) {
const envelope = responseObject(payload);
const status =
response.status === 401
? 401
: typeof envelope.status === "number" && envelope.status !== 200
? envelope.status
: response.status;
if (status === 401) clearUnauthorizedSession(requestToken);
return new ApiError(
status,
W(envelope.msg || envelope.message) || fallbackMessage,
envelope.data,
);
}
async function decodeResponse(
response,
requestToken,
fallbackMessage,
signal,
allowStatuslessDataArray = false,
) {
const payload = await response.json().catch(() => null);
assertRequestActive(signal);
const envelope = responseObject(payload);
if (response.ok && envelope.status === 200) return payload;
if (
response.ok &&
allowStatuslessDataArray &&
!("status" in envelope) &&
Array.isArray(envelope.data)
) {
return { status: 200, msg: "", data: envelope.data };
}
throw responseError(response, payload, requestToken, fallbackMessage);
}
function apiUrl(path, query, keepEmpty = false) {
let url = `${String(config.public.apiBase ?? "")}/console/v1${path}`;
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query ?? {})) {
if (value == null || (value === "" && !keepEmpty)) continue;
if (Array.isArray(value)) {
for (const item of value) {
if (item != null && item !== "")
search.append(`${key}[]`, String(item));
}
} else {
search.append(key, String(value));
}
}
if (search.size) url += `?${search}`;
return url;
}
async function request(method, path, options = {}) {
// Capture identity before the first await, including when logout clears it immediately.
const requestToken = auth.token;
const headers = { Accept: "application/json" };
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
if (options.body !== undefined)
headers["Content-Type"] = "application/json";
try {
const serializedBody =
options.body === undefined ? undefined : JSON.stringify(options.body);
return await withRequestDeadline(async (signal) => {
if (requestToken) {
let purchase;
try {
purchase = Ee(method, path, options.body);
} catch (error) {
throw new ApiError(
422,
error instanceof Error
? error.message
: "购买信息无效,请重新确认后重试",
);
}
if (purchase) {
const authToken = auth.token;
const permission = await request("POST", "/purchase_limit/check", {
body: purchase,
timeoutMs: options.timeoutMs ?? 15_000,
});
assertRequestActive(signal);
if (
localStorage.getItem("jwt") !== requestToken ||
auth.token !== authToken
) {
throw new ApiError(409, "登录状态已变化,请重新确认后重试");
}
if (JSON.stringify(options.body) !== serializedBody) {
throw new ApiError(409, "购买信息已变化,请重新确认后重试");
}
if (!De(permission))
throw new ApiError(502, "限购验证暂不可用,请稍后重试");
if (!permission.allowed)
throw new ApiError(422, permission.message);
}
}
assertRequestActive(signal);
const response = await fetch(
apiUrl(path, options.query, options.keepEmpty),
{
method,
headers,
body: serializedBody,
signal,
},
);
assertRequestActive(signal);
const envelope = await decodeResponse(
response,
requestToken,
response.ok
? "响应格式异常,请稍后重试"
: `请求失败(HTTP ${response.status})`,
signal,
options.allowStatuslessDataArray,
);
assertRequestActive(signal);
if (
method !== "GET" &&
path !== "/purchase_limit/check" &&
!/^\/product\/\d+\/config_option$/.test(path)
) {
_e();
}
return options.raw ? envelope : envelope.data;
}, options.timeoutMs);
} catch (error) {
throw normalizeRequestError(error);
}
}
async function upload(file) {
const body = new FormData();
body.append("file", file);
const headers = { Accept: "application/json" };
const requestToken = auth.token;
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
try {
return await withRequestDeadline(async (signal) => {
const response = await fetch(apiUrl("/upload"), {
method: "POST",
headers,
body,
signal,
});
assertRequestActive(signal);
const envelope = await decodeResponse(
response,
requestToken,
"上传失败",
signal,
);
assertRequestActive(signal);
return envelope.data;
}, FILE_TRANSFER_TIMEOUT_MS);
} catch (error) {
throw normalizeRequestError(error);
}
}
async function download(path, data, filename = "export.xlsx", options = {}) {
const method = options.method ?? "GET";
const headers = { Accept: "*/*" };
const requestToken = auth.token;
if (requestToken) headers.Authorization = `Bearer ${requestToken}`;
if (method === "POST") headers["Content-Type"] = "application/json";
try {
await withRequestDeadline(async (signal) => {
const response = await fetch(
apiUrl(path, method === "GET" ? data : undefined),
{
method,
headers,
...(method === "POST" ? { body: JSON.stringify(data ?? {}) } : {}),
signal,
},
);
assertRequestActive(signal);
const isJson = /\bapplication\/(?:[\w.-]+\+)?json\b/i.test(
response.headers.get("content-type") || "",
);
if (!response.ok || isJson) {
const payload = await response
.clone()
.json()
.catch(() => null);
assertRequestActive(signal);
const envelope = responseObject(payload);
if (
!response.ok ||
(typeof envelope.status === "number" && envelope.status !== 200)
) {
throw responseError(
response,
payload,
requestToken,
`下载失败(HTTP ${response.status})`,
);
}
}
const disposition = response.headers.get("content-disposition") || "";
const match = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(disposition);
if (match?.[1]) {
try {
filename = decodeURIComponent(match[1]);
} catch {
filename = match[1];
}
}
const blob = await response.blob();
assertRequestActive(signal);
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
try {
link.href = objectUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
} finally {
link.remove();
URL.revokeObjectURL(objectUrl);
}
}, options.timeoutMs ?? FILE_TRANSFER_TIMEOUT_MS);
} catch (error) {
throw normalizeRequestError(error);
}
}
return {
identity: () => auth.token,
request,
upload,
download,
get: (path, query, keepEmpty = false, options = {}) =>
request("GET", path, {
...options,
query,
keepEmpty,
}),
getRaw: (path, query, options = {}) =>
request("GET", path, {
...options,
query,
raw: true,
}),
post: (path, body, raw = false, options = {}) =>
request("POST", path, {
...options,
body,
raw,
}),
put: (path, body, query) => request("PUT", path, { body, query }),
del: (path, query, body) => request("DELETE", path, { query, body }),
};
}
var ot = CryptoJS.enc.Utf8.parse(`idcsmart.finance`);
var st = CryptoJS.enc.Utf8.parse(`9311019310287172`);
function ct(e) {
return CryptoJS.AES.encrypt(e, ot, {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv: st,
}).toString();
}
function lt(e, t = `登录失败,请稍后重试`) {
let n = e && typeof e == `object` ? e : {},
r = typeof n.message == `string` ? n.message.trim() : ``,
i = n.data && typeof n.data == `object` ? n.data : {};
return i.need_security_verify || i.ip_exception_verify
? `本次登录需要额外的安全验证,请联系客服协助完成验证。`
: n.status === 429
? `登录尝试过于频繁,请稍后再试`
: n.status === 408 || /超时|timeout/i.test(r)
? `登录请求超时,请检查网络后重试`
: n.status === 0 ||
/网络异常|Failed to fetch|NetworkError|Load failed/i.test(r)
? `网络连接失败,请检查网络后重试`
: (n.status ?? 0) >= 500 ||
/页面错误|响应格式异常|Internal Server Error|Bad Gateway|Service Unavailable/i.test(
r,
)
? `登录服务暂时异常,请稍后重试;若持续失败,请联系客服。`
: !r ||
/<[^>]+>|SQLSTATE|\b(?:TypeError|ReferenceError|Exception)\b/.test(
r,
)
? t
: r;
}
function ut(e) {
return (
typeof e == `string` && e.length > 0 && e.length <= 8192 && !/\s/.test(e)
);
}
function dt(e) {
if (!e || typeof e != `object`) return null;
let t = e,
n = [t, t.data, t.error, t.response];
for (let e of n) {
if (!e || typeof e != `object`) continue;
let t = e,
n = t.data && typeof t.data == `object` ? t.data : t;
if (
n.need_security_verify === !0 ||
n.need_security_verify === 1 ||
n.need_security_verify === `1` ||
(Array.isArray(n.available_methods) && n.available_methods.length > 0)
)
return n;
}
return null;
}
function ft(e) {
let t = dt(e);
if (t)
throw Object.assign(Error(`安全验证尚未完成,请重试`), {
data: t,
});
}
var pt = {
operate_password: `操作密码`,
email_code: `邮箱验证码`,
phone_code: `手机验证码`,
certification: `实名认证`,
};
function mt(e) {
return (
Array.isArray(e.available_methods) ? e.available_methods : []
).flatMap((t) => {
let n =
typeof t == `string`
? {
value: t,
}
: t && typeof t == `object`
? t
: {},
r = String(n.value ?? ``);
return pt[r]
? [
{
value: r,
label: pt[r],
tip: String(n.tip ?? ``),
account: String(n.account ?? ``),
phone_code: String(n.phone_code ?? ``),
security_verify_token: String(
n.security_verify_token ?? e.security_verify_token ?? ``,
),
},
]
: [];
});
}
const useAuth = defineStore("auth", () => {
const token = ref(localStorage.getItem("jwt"));
const overview = ref(null);
const loading = ref(false);
const legacyLoginPending = ref(false);
const isLoggedIn = computed(() => !!token.value);
let overviewPromise = null;
let sessionVersion = 0;
let legacySession = null;
function loginWithJwt(jwt) {
if (!ut(jwt))
throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。");
resetSessionState();
token.value = jwt;
localStorage.setItem("jwt", jwt);
}
function prepareLegacyLogin(jwt = localStorage.getItem("jwt")) {
clear();
legacySession = {
jwt: ut(jwt) ? jwt : null,
version: sessionVersion,
controller: null,
promise: null,
};
legacyLoginPending.value = true;
}
function cancelLegacyLogin() {
legacySession?.controller?.abort();
legacySession = null;
legacyLoginPending.value = false;
}
function verifyLegacyLogin() {
const session = legacySession;
if (!session?.jwt) {
return Promise.reject(
Error("缺少有效登录凭证,请返回原平台重新进入,或使用账号登录。"),
);
}
if (session.promise) return session.promise;
const apiBase = String(useRuntimeConfig().public.apiBase ?? "");
const controller = new AbortController();
session.controller = controller;
const isCurrentSession = () =>
legacySession === session && sessionVersion === session.version;
const promise = withRequestDeadline(
async (signal) => {
let response;
try {
response = await fetch(`${apiBase}/console/v1/index`, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${session.jwt}`,
},
credentials: "omit",
cache: "no-store",
referrerPolicy: "no-referrer",
signal,
});
} catch {
throw Error("网络连接失败或验证超时,请检查网络后重试。");
}
const envelope = await response.json().catch(() => null);
assertRequestActive(signal);
if (!isCurrentSession()) throw Error("登录状态已变化,请重新进入。");
if (response.status === 401 || envelope?.status === 401) {
throw Error("登录凭证已失效,请返回原平台重新进入,或使用账号登录。");
}
const account = envelope?.data?.account;
if (
!response.ok ||
envelope?.status !== 200 ||
!account ||
typeof account !== "object" ||
!Number.isSafeInteger(account.id) ||
account.id <= 0
)
throw Error("暂时无法验证登录身份,请稍后重试。");
loginWithJwt(session.jwt);
overview.value = account;
},
15_000,
controller,
)
.catch((error) => {
if (error instanceof ApiError && error.status === 408) {
throw Error("网络连接失败或验证超时,请检查网络后重试。");
}
throw error;
})
.finally(() => {
if (isCurrentSession()) {
session.controller = null;
session.promise = null;
}
});
session.promise = promise;
return promise;
}
async function login(
account,
password,
captcha = "",
captchaToken = "",
type = "password",
code = "",
phoneCode = "86",
verification,
) {
if (type === "code" && account.includes("@"))
throw Error("验证码登录仅支持手机号");
const api = useApi();
const version = sessionVersion;
loading.value = true;
try {
const result = await api.post(
"/login",
{
type,
account,
...(account.includes("@") ? {} : { phone_code: phoneCode }),
code,
password: type === "password" ? ct(password) : "",
remember_password: "0",
captcha,
token: captchaToken,
...(verification || {}),
},
false,
{ timeoutMs: 15_000 },
);
if (version !== sessionVersion) throw Error("登录状态已变化,请重试");
ft(result);
if (!ut(result?.jwt))
throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。");
loginWithJwt(result.jwt);
} finally {
loading.value = false;
}
}
function loadOverview() {
if (!overviewPromise) {
const version = sessionVersion;
const promise = useApi()
.get("/index", undefined, false, { timeoutMs: 10_000 })
.then((result) => {
if (version !== sessionVersion) throw Error("登录状态已变化,请重试");
overview.value = result.account;
return result.account;
})
.finally(() => {
if (overviewPromise === promise) overviewPromise = null;
});
overviewPromise = promise;
}
return overviewPromise;
}
async function fetchOverview() {
return overview.value ?? loadOverview();
}
async function refreshOverview() {
return loadOverview();
}
function logout() {
// request() captures the original Bearer synchronously; remote revocation never owns local logout.
const revocation = useApi().post("/logout");
clear();
void revocation.catch(() => {});
}
function resetSessionState() {
_e();
cancelLegacyLogin();
sessionVersion += 1;
overview.value = null;
overviewPromise = null;
}
function clear() {
resetSessionState();
token.value = null;
localStorage.removeItem("jwt");
}
return {
token,
overview,
loading,
isLoggedIn,
login,
loginWithJwt,
fetchOverview,
refreshOverview,
logout,
clear,
legacyLoginPending,
prepareLegacyLogin,
verifyLegacyLogin,
cancelLegacyLogin,
};
});
export {
lt as loginErrorMessage,
ApiError,
ye as apiCacheKey,
ve as apiCacheVersion,
W as sanitizeMessage,
de as operationMessage,
mt as securityMethods,
useApi,
Se as writeApiCache,
ft as assertSecurityComplete,
ut as isValidJwt,
xe as readApiCache,
dt as securityChallenge,
useAuth,
_e as clearApiCache,
};