import { marked } from "marked"; const DEFAULT_DEV_API_BASE = "http://localhost:8080"; const DEFAULT_PROD_API_BASE = "https://auth.api.shumengya.top"; export const API_BASE = (() => { const configuredBase = import.meta.env.VITE_API_BASE?.trim(); const fallbackBase = import.meta.env.DEV ? DEFAULT_DEV_API_BASE : DEFAULT_PROD_API_BASE; return (configuredBase || fallbackBase).replace(/\/+$/, ""); })(); /** OAuth 成功回跳:当前页(不含 # fragment),与后端 `allowedReturnPrefixes` 白名单配合 */ export function getOAuthReturnTo() { if (typeof window === "undefined") return ""; return `${window.location.origin}${window.location.pathname}${window.location.search}`; } /** `public/logo192.png`,含 Vite `base` 前缀,避免子路径部署时顶栏/开屏裂图 */ export const LOGO_192_SRC = `${import.meta.env.BASE_URL}logo192.png`; /** 全站随机背景(`GET /api/random?format=json`,见 https://randbg.api.smyhub.com) */ export const RAND_BG_API_ORIGIN = "https://randbg.api.smyhub.com"; const SELF_SERVICE_ACCOUNT_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"; /** 与自助注册规则一致:随机小写字母与数字(默认长度 10) */ export function randomSelfServiceAccount(length = 10) { const n = Math.max(3, Math.min(32, Number(length) || 10)); const buf = new Uint8Array(n); crypto.getRandomValues(buf); let s = ""; for (let i = 0; i < n; i++) { s += SELF_SERVICE_ACCOUNT_ALPHABET[buf[i] % SELF_SERVICE_ACCOUNT_ALPHABET.length]; } return s; } /** 输入时规范为自助注册允许的字符(小写、数字),超长截断 */ export function normalizeSelfServiceAccountInput(raw, maxLen = 32) { const m = Math.max(3, Math.min(32, maxLen)); return String(raw || "") .toLowerCase() .replace(/[^a-z0-9]/g, "") .slice(0, m); } /** 浏览器侧 IP/地理(与后端写入「最后访问」字段配合使用) */ export const CLIENT_GEO_LOOKUP_URL = "https://cf-ip-geo.smyhub.com/api"; export function formatVisitDisplayLocation(payload) { const g = payload?.geo; if (!g || typeof g !== "object") return ""; const parts = [g.countryName, g.regionName, g.cityName].filter( (x) => typeof x === "string" && x.trim() ); return parts.join(" "); } export async function fetchClientVisitMeta(timeoutMs = 2800) { const ctrl = new AbortController(); const id = setTimeout(() => ctrl.abort(), timeoutMs); try { const res = await fetch(CLIENT_GEO_LOOKUP_URL, { credentials: "omit", signal: ctrl.signal }); clearTimeout(id); if (!res.ok) return { ip: "", displayLocation: "" }; const data = await res.json(); const ip = typeof data.ip === "string" ? data.ip.trim() : ""; const displayLocation = formatVisitDisplayLocation(data); return { ip, displayLocation }; } catch { clearTimeout(id); return { ip: "", displayLocation: "" }; } } marked.setOptions({ breaks: true }); export { marked }; export const emptyForm = { account: "", password: "", username: "", email: "", level: 0, sproutCoins: 0, secondaryEmails: "", phone: "", avatarUrl: "", websiteUrl: "", bio: "", banned: false, banReason: "" }; /** 后端封禁响应:`error` + 可选 `banReason`(登录/me 等 403) */ export function formatAuthBanMessage(data) { const base = (data && data.error && String(data.error).trim()) || "account is banned"; const reason = data && data.banReason != null && String(data.banReason).trim(); return reason ? `${base}:${reason}` : base; } /** 展示用:从完整 URL 得到「主机 + 路径」短文案 */ export function formatWebsiteLabel(url) { if (!url || typeof url !== "string") return ""; try { const u = new URL(url); const path = u.pathname === "/" ? "" : u.pathname; return u.host + path; } catch { return url; } } /** 用户 `createdAt`(RFC3339)→ 本地可读注册时间 */ export function formatUserRegisteredAt(iso) { if (!iso || typeof iso !== "string") return "未知"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return new Intl.DateTimeFormat("zh-CN", { dateStyle: "long", timeStyle: "short" }).format(d); } /** RFC3339 / 后端存储时间 → 本地可读(用于应用接入记录等) */ export function formatIsoDateTimeReadable(iso) { if (!iso || typeof iso !== "string") return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "medium" }).format(d); } export const parseEmailList = (value) => value.split(",").map((item) => item.trim()).filter(Boolean); /** 可点击发信:`mailto:`,排除明显非法地址 */ export function mailtoHref(address) { const a = typeof address === "string" ? address.trim() : ""; if (!a || /\s/.test(a) || a.includes("<") || a.includes(">")) return null; const at = a.indexOf("@"); if (at < 1 || at === a.length - 1) return null; return `mailto:${a}`; } export function getPublicAccountFromPathname(pathname) { if (pathname !== "/user" && !pathname.startsWith("/user/")) return ""; const raw = pathname === "/user" ? "" : pathname.slice("/user/".length).split("/")[0]; if (!raw) return ""; try { return decodeURIComponent(raw).trim(); } catch { return raw.trim(); } } export function getAuthFlowFromSearch(search) { const params = new URLSearchParams(search); const redirectUri = (params.get("redirect_uri") || params.get("return_url") || "").trim(); const scopeRaw = (params.get("scope") || "").trim(); const scopes = scopeRaw ? scopeRaw.split(/[\s+]+/).map((s) => s.trim()).filter(Boolean) : []; return { redirectUri, state: (params.get("state") || "").trim(), prompt: (params.get("prompt") || "").trim(), clientId: (params.get("client_id") || "").trim(), clientName: (params.get("client_name") || "").trim(), scopes, scopeRaw }; } /** 授权页展示用:回跳地址可读标签(主机 + 路径) */ export function formatOAuthRedirectLabel(redirectUri) { if (!redirectUri || typeof redirectUri !== "string") return "—"; const t = redirectUri.trim(); if (!t) return "—"; try { const u = new URL(t, typeof window !== "undefined" ? window.location.href : "https://example.com"); const path = u.pathname === "/" ? "" : u.pathname; return `${u.host}${path}` || u.host; } catch { return t.length > 64 ? `${t.slice(0, 61)}…` : t; } } const AUTH_CLIENT_ID_KEY = "sproutgate_auth_client_id"; const AUTH_CLIENT_NAME_KEY = "sproutgate_auth_client_name"; /** 从统一登录 URL 上的 `client_id` / `client_name` 写入 session,后续请求带 `X-Auth-Client` 头以便记录接入应用 */ export function persistAuthClientFromFlow(authFlow) { if (!authFlow?.clientId) return; try { sessionStorage.setItem(AUTH_CLIENT_ID_KEY, authFlow.clientId); if (authFlow.clientName) sessionStorage.setItem(AUTH_CLIENT_NAME_KEY, authFlow.clientName); } catch { /* ignore */ } } export function authClientFetchHeaders() { try { const id = (sessionStorage.getItem(AUTH_CLIENT_ID_KEY) || "").trim(); const name = (sessionStorage.getItem(AUTH_CLIENT_NAME_KEY) || "").trim(); const h = {}; if (id) h["X-Auth-Client"] = id; if (name) h["X-Auth-Client-Name"] = name; return h; } catch { return {}; } } export function clearAuthClientContext() { try { sessionStorage.removeItem(AUTH_CLIENT_ID_KEY); sessionStorage.removeItem(AUTH_CLIENT_NAME_KEY); } catch { /* ignore */ } } export function buildAuthCallbackUrl(redirectUri, payload) { const url = new URL(redirectUri, window.location.href); const hashParams = new URLSearchParams(url.hash ? url.hash.slice(1) : ""); Object.entries(payload).forEach(([key, value]) => { if (value === undefined || value === null || value === "") return; hashParams.set(key, String(value)); }); url.hash = hashParams.toString(); return url.toString(); } /** 用户拒绝授权:回跳到应用地址,在 URL hash 中携带 OAuth 风格 error(与成功回跳一致使用 fragment) */ export function buildAuthDenyCallbackUrl(redirectUri, state = "") { if (!redirectUri || typeof redirectUri !== "string") return ""; const url = new URL(redirectUri, window.location.href); const hashParams = new URLSearchParams(); hashParams.set("error", "access_denied"); hashParams.set("error_description", "resource_owner_denied"); if (state) hashParams.set("state", String(state)); url.hash = hashParams.toString(); return url.toString(); }