🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
import { currentUser } from "@workspace/ui/services/admin/user";
import { isBrowser } from "@workspace/ui/utils/index";
import { create } from "zustand";
export interface GlobalStore {
common: API.GetGlobalConfigResponse;
user?: API.User;
setCommon: (common: Partial<API.GetGlobalConfigResponse>) => void;
setUser: (user?: API.User) => void;
getUserInfo: () => Promise<void>;
getUserSubscribe: (uuid: string, type?: string) => string[];
getAppSubLink: (url: string, schema?: string) => string;
}
/**
* Extracts the full domain or root domain from a URL.
*
* @param url - The URL to extract the domain from.
* @param extractRoot - If true, extracts the root domain (e.g., example.com). If false, extracts the full domain (e.g., sub.example.com).
* @returns The extracted domain or root domain, or null if the URL is invalid.
*/
export function extractDomain(url: string, extractRoot = true): string | null {
try {
const { hostname } = new URL(url);
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
return hostname;
}
const domainParts = hostname.split(".").filter(Boolean);
if (extractRoot && domainParts.length > 2) {
return domainParts.slice(-2).join(".");
}
return hostname;
} catch (error) {
console.error("Invalid URL:", error);
return null;
}
}
export const useGlobalStore = create<GlobalStore>((set, get) => ({
common: {
site: {
host: "",
site_name: "",
site_desc: "",
site_logo: "",
keywords: "",
custom_html: "",
custom_data: "",
},
verify: {
turnstile_site_key: "",
enable_login_verify: false,
enable_register_verify: false,
enable_reset_password_verify: false,
},
auth: {
mobile: {
enable: false,
enable_whitelist: false,
whitelist: [],
},
email: {
enable: false,
enable_verify: false,
enable_domain_suffix: false,
domain_suffix_list: "",
},
register: {
stop_register: false,
enable_ip_register_limit: false,
ip_register_limit: 0,
ip_register_limit_duration: 0,
},
device: {
enable: false,
show_ads: false,
enable_security: false,
only_real_device: false,
},
},
invite: {
forced_invite: false,
referral_percentage: 0,
only_first_purchase: false,
},
currency: {
currency_unit: "USD",
currency_symbol: "$",
},
subscribe: {
single_model: false,
subscribe_path: "",
subscribe_domain: "",
pan_domain: false,
user_agent_limit: false,
user_agent_list: "",
},
verify_code: {
verify_code_expire_time: 5,
verify_code_limit: 15,
verify_code_interval: 60,
},
oauth_methods: [],
web_ad: false,
},
user: undefined,
setCommon: (common) =>
set((state) => ({
common: {
...state.common,
...common,
},
})),
setUser: (user) => set({ user }),
getUserInfo: async () => {
try {
const { data } = await currentUser();
set({ user: data.data });
} catch (error) {
console.error("Failed to refresh user:", error);
}
},
getUserSubscribe: (uuid: string, type?: string) => {
const { pan_domain, subscribe_domain, subscribe_path } =
get().common.subscribe || {};
const domains = subscribe_domain
? subscribe_domain.split("\n")
: [extractDomain(window.location.origin, pan_domain)];
return domains.map((domain) => {
if (pan_domain) {
if (type) return `https://${uuid}.${type}.${domain}`;
return `https://${uuid}.${domain}`;
}
if (type)
return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`;
return `https://${domain}${subscribe_path}?token=${uuid}`;
});
},
getAppSubLink: (url: string, schema?: string) => {
const name = get().common?.site?.site_name || "";
if (!schema) return "url";
try {
let result = schema.replace(/\${url}/g, url).replace(/\${name}/g, name);
const maxLoop = 10;
let prev: string;
let loop = 0;
do {
prev = result;
result = result.replace(
/\${encodeURIComponent\(JSON\.stringify\(([^)]+)\)\)}/g,
(match, expr) => {
try {
const processedExpr = expr
.replace(/url/g, `"${url}"`)
.replace(/name/g, `"${name}"`);
if (processedExpr.includes("server_remote")) {
const serverRemoteValue = `${url}, tag=${name}`;
return encodeURIComponent(
JSON.stringify({ server_remote: [serverRemoteValue] })
);
}
const obj = eval(`(${processedExpr})`);
return encodeURIComponent(JSON.stringify(obj));
} catch {
return match;
}
}
);
result = result.replace(
/\${encodeURIComponent\(([^)]+)\)}/g,
(match, expr) => {
if (expr === "url") return encodeURIComponent(url);
if (expr === "name") return encodeURIComponent(name);
try {
return encodeURIComponent(expr);
} catch {
return match;
}
}
);
result = result.replace(
/\${window\.btoa\(([^)]+)\)}/g,
(match, expr) => {
const btoa = isBrowser() ? window.btoa : (str: string) => str;
if (expr === "url") return btoa(url);
if (expr === "name") return btoa(name);
try {
return btoa(expr);
} catch {
return match;
}
}
);
result = result.replace(
/\${JSON\.stringify\(([^}]+)\)}/g,
(match, expr) => {
try {
const processedExpr = expr
.replace(/url/g, `"${url}"`)
.replace(/name/g, `"${name}"`);
if (processedExpr.includes("server_remote")) {
const serverRemoteValue = `${url}, tag=${name}`;
return JSON.stringify({ server_remote: [serverRemoteValue] });
}
const result = eval(`(${processedExpr})`);
return JSON.stringify(result);
} catch {
return match;
}
}
);
loop++;
} while (result !== prev && loop < maxLoop);
return result;
} catch (_error) {
return "";
}
},
}));
+143
View File
@@ -0,0 +1,143 @@
import {
filterNodeList,
queryNodeTag,
} from "@workspace/ui/services/admin/server";
import { create } from "zustand";
interface NodeState {
// Data
nodes: API.Node[];
tags: string[];
// Loading states
loading: boolean;
loadingTags: boolean;
loaded: boolean;
loadedTags: boolean;
// Actions
fetchNodes: () => Promise<void>;
fetchTags: () => Promise<void>;
// Getters
getNodeById: (nodeId: number) => API.Node | undefined;
isProtocolUsedInNodes: (serverId: number, protocolType: string) => boolean;
isServerReferencedByNodes: (serverId: number) => boolean;
getNodesByTag: (tag: string) => API.Node[];
getNodesWithoutTags: () => API.Node[];
getNodeTags: () => string[];
getAllAvailableTags: () => string[];
}
export const useNodeStore = create<NodeState>((set, get) => ({
// Initial state
nodes: [],
tags: [],
loading: false,
loadingTags: false,
loaded: false,
loadedTags: false,
// Actions
fetchNodes: async () => {
if (get().loading) return;
set({ loading: true });
try {
const { data } = await filterNodeList({ page: 1, size: 999_999_999 });
set({
nodes: data?.data?.list || [],
loaded: true,
});
} catch (_error) {
// Handle error silently
set({ loaded: true });
} finally {
set({ loading: false });
}
},
fetchTags: async () => {
if (get().loadingTags) return;
set({ loadingTags: true });
try {
const { data } = await queryNodeTag();
set({
tags: data?.data?.tags || [],
loadedTags: true,
});
} catch (_error) {
// Handle error silently
set({ loadedTags: true });
} finally {
set({ loadingTags: false });
}
},
// Getters
getNodeById: (nodeId: number) => get().nodes.find((n) => n.id === nodeId),
isProtocolUsedInNodes: (serverId: number, protocolType: string) =>
get().nodes.some(
(node) => node.server_id === serverId && node.protocol === protocolType
),
isServerReferencedByNodes: (serverId: number) =>
get().nodes.some((node) => node.server_id === serverId),
getNodesByTag: (tag: string) =>
get().nodes.filter((node) => (node.tags || []).includes(tag)),
getNodesWithoutTags: () =>
get().nodes.filter((node) => (node.tags || []).length === 0),
getNodeTags: () =>
Array.from(
new Set(
get()
.nodes.flatMap((node) => (Array.isArray(node.tags) ? node.tags : []))
.filter(Boolean)
)
) as string[],
getAllAvailableTags: () => {
const nodeExtractedTags = get().getNodeTags();
const allApiTags = get().tags;
return Array.from(new Set([...allApiTags, ...nodeExtractedTags])).filter(
Boolean
);
},
}));
export const useNode = () => {
const store = useNodeStore();
// Auto-fetch nodes and tags
if (!(store.loaded || store.loading)) {
store.fetchNodes();
}
if (!(store.loadedTags || store.loadingTags)) {
store.fetchTags();
}
return {
nodes: store.nodes,
tags: store.tags,
loading: store.loading,
loadingTags: store.loadingTags,
loaded: store.loaded,
loadedTags: store.loadedTags,
fetchNodes: store.fetchNodes,
fetchTags: store.fetchTags,
getNodeById: store.getNodeById,
isProtocolUsedInNodes: store.isProtocolUsedInNodes,
isServerReferencedByNodes: store.isServerReferencedByNodes,
getNodesByTag: store.getNodesByTag,
getNodesWithoutTags: store.getNodesWithoutTags,
getNodeTags: store.getNodeTags,
getAllAvailableTags: store.getAllAvailableTags,
};
};
export default useNodeStore;
+112
View File
@@ -0,0 +1,112 @@
import { filterServerList } from "@workspace/ui/services/admin/server";
import { create } from "zustand";
interface ServerState {
// Data
servers: API.Server[];
// Loading states
loading: boolean;
loaded: boolean;
// Actions
fetchServers: () => Promise<void>;
// Getters
getServerById: (serverId: number) => API.Server | undefined;
getServerName: (serverId?: number) => string;
getServerAddress: (serverId?: number) => string;
getServerEnabledProtocols: (serverId: number) => API.Protocol[];
getProtocolPort: (serverId?: number, protocol?: string) => string;
getAvailableProtocols: (
serverId?: number
) => Array<{ protocol: string; port: number }>;
}
export const useServerStore = create<ServerState>((set, get) => ({
// Initial state
servers: [],
loading: false,
loaded: false,
// Actions
fetchServers: async () => {
if (get().loading) return;
set({ loading: true });
try {
const { data } = await filterServerList({ page: 1, size: 999_999_999 });
set({
servers: data?.data?.list || [],
loaded: true,
});
} catch (_error) {
// Handle error silently
set({ loaded: true });
} finally {
set({ loading: false });
}
},
// Getters
getServerById: (serverId: number) =>
get().servers.find((s) => s.id === serverId),
getServerName: (serverId?: number) => {
if (!serverId) return "—";
const server = get().servers.find((s) => s.id === serverId);
return server?.name ?? `#${serverId}`;
},
getServerAddress: (serverId?: number) => {
if (!serverId) return "—";
const server = get().servers.find((s) => s.id === serverId);
return server?.address ?? "—";
},
getServerEnabledProtocols: (serverId: number) => {
const server = get().servers.find((s) => s.id === serverId);
return server?.protocols?.filter((p) => p.enable) || [];
},
getProtocolPort: (serverId?: number, protocol?: string) => {
if (!(serverId && protocol)) return "—";
const enabledProtocols = get().getServerEnabledProtocols(serverId);
const protocolConfig = enabledProtocols.find((p) => p.type === protocol);
return protocolConfig?.port ? String(protocolConfig.port) : "—";
},
getAvailableProtocols: (serverId?: number) => {
if (!serverId) return [];
return get()
.getServerEnabledProtocols(serverId)
.map((p) => ({
protocol: p.type,
port: p.port,
}));
},
}));
export const useServer = () => {
const store = useServerStore();
// Auto-fetch servers
if (!(store.loaded || store.loading)) {
store.fetchServers();
}
return {
servers: store.servers,
loading: store.loading,
loaded: store.loaded,
fetchServers: store.fetchServers,
getServerById: store.getServerById,
getServerName: store.getServerName,
getServerAddress: store.getServerAddress,
getServerEnabledProtocols: store.getServerEnabledProtocols,
getProtocolPort: store.getProtocolPort,
getAvailableProtocols: store.getAvailableProtocols,
};
};
export default useServerStore;
+84
View File
@@ -0,0 +1,84 @@
import { create } from "zustand";
// Fixed remote stats endpoint and required header
export const REQUIRED_HEADER_NAME = "stats";
export const REQUIRED_HEADER_VALUE = "ppanel.dev";
const STATS_URL = "https://stats.ppanel.dev";
const STATS_LOADED_KEY = "ppanel:stats:loaded";
interface StatsState {
loading: boolean;
loaded: boolean;
stats: () => Promise<void>;
}
async function hashHostname(hostname: string): Promise<string> {
try {
const encoder = new TextEncoder();
const data = encoder.encode(hostname);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
} catch (_e) {
return "";
}
}
export const useStatsStore = create<StatsState>((set) => ({
loading: false,
loaded:
typeof window !== "undefined"
? Boolean(window.localStorage.getItem(STATS_LOADED_KEY))
: false,
stats: async () => {
// if already recorded, skip
if (typeof window !== "undefined") {
try {
if (window.localStorage.getItem(STATS_LOADED_KEY)) return;
} catch {
/* empty */
}
}
set({ loading: true });
try {
const hostname =
typeof window !== "undefined" && window.location
? window.location.hostname
: "";
const domain = hostname ? await hashHostname(hostname) : "";
await fetch(STATS_URL, {
method: "POST",
headers: {
[REQUIRED_HEADER_NAME]: REQUIRED_HEADER_VALUE,
"Content-Type": "application/json",
},
body: JSON.stringify({
domain,
}),
});
set({ loaded: true });
if (typeof window !== "undefined") {
try {
window.localStorage.setItem(STATS_LOADED_KEY, "1");
} catch {
/* empty */
}
}
} catch (_error) {
// treat as completed to avoid repeated attempts
set({ loaded: false });
if (typeof window !== "undefined") {
try {
window.localStorage.setItem(STATS_LOADED_KEY, "0");
} catch {
/* empty */
}
}
} finally {
set({ loading: false });
}
},
}));
+74
View File
@@ -0,0 +1,74 @@
import { getSubscribeList } from "@workspace/ui/services/admin/subscribe";
import { create } from "zustand";
interface SubscribeState {
// Data
subscribes: API.SubscribeItem[];
// Loading states
loading: boolean;
loaded: boolean;
// Actions
fetchSubscribes: () => Promise<void>;
// Getters
getSubscribeName: (subscribeId?: number) => string;
getSubscribeById: (subscribeId: number) => API.SubscribeItem | undefined;
}
export const useSubscribeStore = create<SubscribeState>((set, get) => ({
// Initial state
subscribes: [],
loading: false,
loaded: false,
// Actions
fetchSubscribes: async () => {
if (get().loading) return;
set({ loading: true });
try {
const { data } = await getSubscribeList({ page: 1, size: 999_999_999 });
set({
subscribes: data?.data?.list || [],
loaded: true,
});
} catch (_error) {
// Handle error silently
set({ loaded: true });
} finally {
set({ loading: false });
}
},
// Getters
getSubscribeName: (subscribeId?: number) => {
if (!subscribeId) return "--";
const subscribe = get().subscribes.find((s) => s.id === subscribeId);
return subscribe?.name ?? `Subscribe ${subscribeId}`;
},
getSubscribeById: (subscribeId: number) =>
get().subscribes.find((s) => s.id === subscribeId),
}));
export const useSubscribe = () => {
const store = useSubscribeStore();
// Auto-fetch subscribes
if (!(store.loaded || store.loading)) {
store.fetchSubscribes();
}
return {
subscribes: store.subscribes,
loading: store.loading,
loaded: store.loaded,
fetchSubscribes: store.fetchSubscribes,
getSubscribeName: store.getSubscribeName,
getSubscribeById: store.getSubscribeById,
};
};
export default useSubscribeStore;