🎉 feat: initialization
This commit is contained in:
@@ -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 "";
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user