mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
feat(frontend): extend Zod validation to remaining query/mutation hooks
Adds Zod schemas for client/inbound/xray/node-probe endpoints and wires useNodeMutations, useClients, useInbounds, useXraySetting, useDatepicker through parseMsg. Drops the duplicated per-file ApiMsg<T> interfaces and the local ClientRecord / OutboundTrafficRow / XraySettingsValue / DefaultsPayload declarations in favour of schema-inferred types re-exported from the new src/schemas/ modules. API boundary now validates: clients list/paged, clients onlines, clients lastOnline, clients get/hydrate, inbounds slim, inbounds get, inbounds options, defaultSettings, xray config, xray outbounds traffic, xray testOutbound, xray getXrayResult, getDefaultJsonConfig, nodes probe, nodes test. Mutation responses that consume obj (bulkAdjust, delDepleted, nodes probe / test) get response validation; pass-through mutations stay agnostic. NodeFormModal type-aligned to Msg<ProbeResult>.
This commit is contained in:
parent
6846fac1cc
commit
d00ddc3f58
12 changed files with 350 additions and 226 deletions
|
|
@ -1,21 +1,12 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil, Msg } from '@/utils';
|
||||||
|
import { parseMsg } from '@/utils/zodValidate';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
import { ProbeResultSchema, type ProbeResult } from '@/schemas/node';
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
export type { ProbeResult };
|
||||||
success?: boolean;
|
|
||||||
msg?: string;
|
|
||||||
obj?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProbeResult {
|
|
||||||
status: string;
|
|
||||||
latencyMs?: number;
|
|
||||||
xrayVersion?: string;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNodeMutations() {
|
export function useNodeMutations() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
@ -23,31 +14,33 @@ export function useNodeMutations() {
|
||||||
|
|
||||||
const createMut = useMutation({
|
const createMut = useMutation({
|
||||||
mutationFn: (payload: Partial<NodeRecord>) =>
|
mutationFn: (payload: Partial<NodeRecord>) =>
|
||||||
HttpUtil.post('/panel/api/nodes/add', payload) as Promise<ApiMsg>,
|
HttpUtil.post('/panel/api/nodes/add', payload),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
mutationFn: ({ id, payload }: { id: number; payload: Partial<NodeRecord> }) =>
|
mutationFn: ({ id, payload }: { id: number; payload: Partial<NodeRecord> }) =>
|
||||||
HttpUtil.post(`/panel/api/nodes/update/${id}`, payload) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/nodes/update/${id}`, payload),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeMut = useMutation({
|
const removeMut = useMutation({
|
||||||
mutationFn: (id: number) =>
|
mutationFn: (id: number) =>
|
||||||
HttpUtil.post(`/panel/api/nodes/del/${id}`) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/nodes/del/${id}`),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const setEnableMut = useMutation({
|
const setEnableMut = useMutation({
|
||||||
mutationFn: ({ id, enable }: { id: number; enable: boolean }) =>
|
mutationFn: ({ id, enable }: { id: number; enable: boolean }) =>
|
||||||
HttpUtil.post(`/panel/api/nodes/setEnable/${id}`, { enable }) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/nodes/setEnable/${id}`, { enable }),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const probeMut = useMutation({
|
const probeMut = useMutation({
|
||||||
mutationFn: (id: number) =>
|
mutationFn: async (id: number): Promise<Msg<ProbeResult>> => {
|
||||||
HttpUtil.post(`/panel/api/nodes/probe/${id}`) as Promise<ApiMsg<ProbeResult>>,
|
const raw = await HttpUtil.post(`/panel/api/nodes/probe/${id}`);
|
||||||
|
return parseMsg(raw, ProbeResultSchema, 'nodes/probe');
|
||||||
|
},
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -57,7 +50,9 @@ export function useNodeMutations() {
|
||||||
remove: (id: number) => removeMut.mutateAsync(id),
|
remove: (id: number) => removeMut.mutateAsync(id),
|
||||||
setEnable: (id: number, enable: boolean) => setEnableMut.mutateAsync({ id, enable }),
|
setEnable: (id: number, enable: boolean) => setEnableMut.mutateAsync({ id, enable }),
|
||||||
probe: (id: number) => probeMut.mutateAsync(id),
|
probe: (id: number) => probeMut.mutateAsync(id),
|
||||||
testConnection: (payload: Partial<NodeRecord>) =>
|
testConnection: async (payload: Partial<NodeRecord>): Promise<Msg<ProbeResult>> => {
|
||||||
HttpUtil.post('/panel/api/nodes/test', payload) as Promise<ApiMsg<ProbeResult>>,
|
const raw = await HttpUtil.post('/panel/api/nodes/test', payload);
|
||||||
|
return parseMsg(raw, ProbeResultSchema, 'nodes/test');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,30 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil, Msg } from '@/utils';
|
||||||
|
import { parseMsg } from '@/utils/zodValidate';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
|
import {
|
||||||
|
ClientHydrateSchema,
|
||||||
|
ClientPageResponseSchema,
|
||||||
|
InboundOptionsSchema,
|
||||||
|
OnlinesSchema,
|
||||||
|
BulkAdjustResultSchema,
|
||||||
|
DelDepletedResultSchema,
|
||||||
|
type ClientHydrate,
|
||||||
|
type ClientRecord,
|
||||||
|
type ClientTraffic,
|
||||||
|
type ClientsSummary,
|
||||||
|
type ClientPageResponse,
|
||||||
|
type InboundOption,
|
||||||
|
type BulkAdjustResult,
|
||||||
|
} from '@/schemas/client';
|
||||||
|
import { DefaultsPayloadSchema } from '@/schemas/defaults';
|
||||||
|
|
||||||
|
export type { ClientRecord, ClientTraffic, ClientsSummary, InboundOption };
|
||||||
|
|
||||||
const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
|
const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } } as const;
|
||||||
|
|
||||||
export interface ClientTraffic {
|
|
||||||
up?: number;
|
|
||||||
down?: number;
|
|
||||||
total?: number;
|
|
||||||
expiryTime?: number;
|
|
||||||
enable?: boolean;
|
|
||||||
lastOnline?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientRecord {
|
|
||||||
email: string;
|
|
||||||
subId?: string;
|
|
||||||
uuid?: string;
|
|
||||||
password?: string;
|
|
||||||
auth?: string;
|
|
||||||
flow?: string;
|
|
||||||
totalGB?: number;
|
|
||||||
expiryTime?: number;
|
|
||||||
limitIp?: number;
|
|
||||||
tgId?: number | string;
|
|
||||||
comment?: string;
|
|
||||||
enable?: boolean;
|
|
||||||
inboundIds?: number[];
|
|
||||||
traffic?: ClientTraffic;
|
|
||||||
reverse?: { tag?: string };
|
|
||||||
createdAt?: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InboundOption {
|
|
||||||
id: number;
|
|
||||||
remark?: string;
|
|
||||||
protocol?: string;
|
|
||||||
port?: number;
|
|
||||||
tlsFlowCapable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
|
||||||
success?: boolean;
|
|
||||||
msg?: string;
|
|
||||||
obj?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SubSettings {
|
interface SubSettings {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
subURI: string;
|
subURI: string;
|
||||||
|
|
@ -68,24 +43,6 @@ export interface ClientQueryParams {
|
||||||
order?: 'ascend' | 'descend';
|
order?: 'ascend' | 'descend';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientsSummary {
|
|
||||||
total: number;
|
|
||||||
active: number;
|
|
||||||
online: string[];
|
|
||||||
depleted: string[];
|
|
||||||
expiring: string[];
|
|
||||||
deactive: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ClientPageResponse {
|
|
||||||
items: ClientRecord[];
|
|
||||||
total: number;
|
|
||||||
filtered: number;
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
summary?: ClientsSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_QUERY: ClientQueryParams = { page: 1, pageSize: 25 };
|
const DEFAULT_QUERY: ClientQueryParams = { page: 1, pageSize: 25 };
|
||||||
const DEFAULT_SUMMARY: ClientsSummary = {
|
const DEFAULT_SUMMARY: ClientsSummary = {
|
||||||
total: 0, active: 0, online: [], depleted: [], expiring: [], deactive: [],
|
total: 0, active: 0, online: [], depleted: [], expiring: [], deactive: [],
|
||||||
|
|
@ -106,21 +63,25 @@ function buildQS(p: ClientQueryParams): string {
|
||||||
|
|
||||||
async function fetchClientPage(params: ClientQueryParams): Promise<ClientPageResponse> {
|
async function fetchClientPage(params: ClientQueryParams): Promise<ClientPageResponse> {
|
||||||
const qs = buildQS(params);
|
const qs = buildQS(params);
|
||||||
const msg = await HttpUtil.get(`/panel/api/clients/list/paged?${qs}`, undefined, { silent: true }) as ApiMsg<ClientPageResponse>;
|
const msg = await HttpUtil.get(`/panel/api/clients/list/paged?${qs}`, undefined, { silent: true });
|
||||||
if (!msg?.success || !msg.obj) throw new Error(msg?.msg || 'Failed to fetch clients');
|
if (!msg?.success || !msg.obj) throw new Error(msg?.msg || 'Failed to fetch clients');
|
||||||
return msg.obj;
|
const validated = parseMsg(msg, ClientPageResponseSchema, 'clients/list/paged');
|
||||||
|
if (!validated.obj) throw new Error('Empty clients response');
|
||||||
|
return validated.obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchInboundOptions(): Promise<InboundOption[]> {
|
async function fetchInboundOptions(): Promise<InboundOption[]> {
|
||||||
const msg = await HttpUtil.get('/panel/api/inbounds/options', undefined, { silent: true }) as ApiMsg<InboundOption[]>;
|
const msg = await HttpUtil.get('/panel/api/inbounds/options', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch inbound options');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch inbound options');
|
||||||
return Array.isArray(msg.obj) ? msg.obj : [];
|
const validated = parseMsg(msg, InboundOptionsSchema, 'inbounds/options');
|
||||||
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDefaults(): Promise<Record<string, unknown>> {
|
async function fetchDefaults(): Promise<Record<string, unknown>> {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true }) as ApiMsg<Record<string, unknown>>;
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
||||||
return msg.obj || {};
|
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||||
|
return validated.obj || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useClients() {
|
export function useClients() {
|
||||||
|
|
@ -168,9 +129,10 @@ export function useClients() {
|
||||||
const onlinesQuery = useQuery({
|
const onlinesQuery = useQuery({
|
||||||
queryKey: keys.clients.onlines(),
|
queryKey: keys.clients.onlines(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const msg = await HttpUtil.post('/panel/api/clients/onlines', undefined, { silent: true }) as ApiMsg<string[]>;
|
const msg = await HttpUtil.post('/panel/api/clients/onlines', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch onlines');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch onlines');
|
||||||
return Array.isArray(msg.obj) ? msg.obj : [];
|
const validated = parseMsg(msg, OnlinesSchema, 'clients/onlines');
|
||||||
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||||
},
|
},
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
|
|
@ -208,22 +170,23 @@ export function useClients() {
|
||||||
await invalidateAll();
|
await invalidateAll();
|
||||||
}, [invalidateAll]);
|
}, [invalidateAll]);
|
||||||
|
|
||||||
const hydrate = useCallback(async (email: string): Promise<{ client: ClientRecord; inboundIds: number[] } | null> => {
|
const hydrate = useCallback(async (email: string): Promise<ClientHydrate | null> => {
|
||||||
if (!email) return null;
|
if (!email) return null;
|
||||||
const msg = await HttpUtil.get(`/panel/api/clients/get/${encodeURIComponent(email)}`) as ApiMsg<{ client: ClientRecord; inboundIds: number[] }>;
|
const msg = await HttpUtil.get(`/panel/api/clients/get/${encodeURIComponent(email)}`);
|
||||||
if (!msg?.success || !msg.obj) return null;
|
if (!msg?.success || !msg.obj) return null;
|
||||||
return msg.obj;
|
const validated = parseMsg(msg, ClientHydrateSchema, 'clients/get');
|
||||||
|
return validated.obj;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const createMut = useMutation({
|
const createMut = useMutation({
|
||||||
mutationFn: (payload: unknown) =>
|
mutationFn: (payload: unknown) =>
|
||||||
HttpUtil.post('/panel/api/clients/add', payload, JSON_HEADERS) as Promise<ApiMsg>,
|
HttpUtil.post('/panel/api/clients/add', payload, JSON_HEADERS),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMut = useMutation({
|
const updateMut = useMutation({
|
||||||
mutationFn: ({ email, client }: { email: string; client: unknown }) =>
|
mutationFn: ({ email, client }: { email: string; client: unknown }) =>
|
||||||
HttpUtil.post(`/panel/api/clients/update/${encodeURIComponent(email)}`, client, JSON_HEADERS) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/clients/update/${encodeURIComponent(email)}`, client, JSON_HEADERS),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -232,7 +195,7 @@ export function useClients() {
|
||||||
const url = keepTraffic
|
const url = keepTraffic
|
||||||
? `/panel/api/clients/del/${encodeURIComponent(email)}?keepTraffic=1`
|
? `/panel/api/clients/del/${encodeURIComponent(email)}?keepTraffic=1`
|
||||||
: `/panel/api/clients/del/${encodeURIComponent(email)}`;
|
: `/panel/api/clients/del/${encodeURIComponent(email)}`;
|
||||||
return HttpUtil.post(url) as Promise<ApiMsg>;
|
return HttpUtil.post(url);
|
||||||
},
|
},
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
@ -242,7 +205,7 @@ export function useClients() {
|
||||||
const suffix = keepTraffic ? '?keepTraffic=1' : '';
|
const suffix = keepTraffic ? '?keepTraffic=1' : '';
|
||||||
const results = await Promise.all(emails.map((email) => {
|
const results = await Promise.all(emails.map((email) => {
|
||||||
const url = `/panel/api/clients/del/${encodeURIComponent(email)}${suffix}`;
|
const url = `/panel/api/clients/del/${encodeURIComponent(email)}${suffix}`;
|
||||||
return HttpUtil.post(url, undefined, { silent: true }) as Promise<ApiMsg>;
|
return HttpUtil.post(url, undefined, { silent: true });
|
||||||
}));
|
}));
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
|
@ -250,54 +213,55 @@ export function useClients() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const bulkAdjustMut = useMutation({
|
const bulkAdjustMut = useMutation({
|
||||||
mutationFn: (payload: { emails: string[]; addDays: number; addBytes: number }) =>
|
mutationFn: async (payload: { emails: string[]; addDays: number; addBytes: number }): Promise<Msg<BulkAdjustResult>> => {
|
||||||
HttpUtil.post(
|
const raw = await HttpUtil.post('/panel/api/clients/bulkAdjust', payload, JSON_HEADERS);
|
||||||
'/panel/api/clients/bulkAdjust',
|
return parseMsg(raw, BulkAdjustResultSchema, 'clients/bulkAdjust');
|
||||||
payload,
|
},
|
||||||
JSON_HEADERS,
|
|
||||||
) as Promise<ApiMsg<{ adjusted: number; skipped?: { email: string; reason: string }[] }>>,
|
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const attachMut = useMutation({
|
const attachMut = useMutation({
|
||||||
mutationFn: ({ email, inboundIds }: { email: string; inboundIds: number[] }) =>
|
mutationFn: ({ email, inboundIds }: { email: string; inboundIds: number[] }) =>
|
||||||
HttpUtil.post(`/panel/api/clients/${encodeURIComponent(email)}/attach`, { inboundIds }, JSON_HEADERS) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/clients/${encodeURIComponent(email)}/attach`, { inboundIds }, JSON_HEADERS),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const detachMut = useMutation({
|
const detachMut = useMutation({
|
||||||
mutationFn: ({ email, inboundIds }: { email: string; inboundIds: number[] }) =>
|
mutationFn: ({ email, inboundIds }: { email: string; inboundIds: number[] }) =>
|
||||||
HttpUtil.post(`/panel/api/clients/${encodeURIComponent(email)}/detach`, { inboundIds }, JSON_HEADERS) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/clients/${encodeURIComponent(email)}/detach`, { inboundIds }, JSON_HEADERS),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetTrafficMut = useMutation({
|
const resetTrafficMut = useMutation({
|
||||||
mutationFn: (email: string) =>
|
mutationFn: (email: string) =>
|
||||||
HttpUtil.post(`/panel/api/clients/resetTraffic/${encodeURIComponent(email)}`) as Promise<ApiMsg>,
|
HttpUtil.post(`/panel/api/clients/resetTraffic/${encodeURIComponent(email)}`),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetAllTrafficsMut = useMutation({
|
const resetAllTrafficsMut = useMutation({
|
||||||
mutationFn: () => HttpUtil.post('/panel/api/clients/resetAllTraffics') as Promise<ApiMsg>,
|
mutationFn: () => HttpUtil.post('/panel/api/clients/resetAllTraffics'),
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const delDepletedMut = useMutation({
|
const delDepletedMut = useMutation({
|
||||||
mutationFn: () => HttpUtil.post('/panel/api/clients/delDepleted') as Promise<ApiMsg<{ deleted?: number }>>,
|
mutationFn: async () => {
|
||||||
|
const raw = await HttpUtil.post('/panel/api/clients/delDepleted');
|
||||||
|
return parseMsg(raw, DelDepletedResultSchema, 'clients/delDepleted');
|
||||||
|
},
|
||||||
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
|
||||||
});
|
});
|
||||||
|
|
||||||
const create = useCallback((payload: unknown) => createMut.mutateAsync(payload), [createMut]);
|
const create = useCallback((payload: unknown) => createMut.mutateAsync(payload), [createMut]);
|
||||||
const update = useCallback((email: string, client: unknown) => {
|
const update = useCallback((email: string, client: unknown) => {
|
||||||
if (!email) return Promise.resolve(null as unknown as ApiMsg);
|
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
return updateMut.mutateAsync({ email, client });
|
return updateMut.mutateAsync({ email, client });
|
||||||
}, [updateMut]);
|
}, [updateMut]);
|
||||||
const remove = useCallback((email: string, keepTraffic = false) => {
|
const remove = useCallback((email: string, keepTraffic = false) => {
|
||||||
if (!email) return Promise.resolve(null as unknown as ApiMsg);
|
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
return removeMut.mutateAsync({ email, keepTraffic });
|
return removeMut.mutateAsync({ email, keepTraffic });
|
||||||
}, [removeMut]);
|
}, [removeMut]);
|
||||||
const removeMany = useCallback((emails: string[], keepTraffic = false) => {
|
const removeMany = useCallback((emails: string[], keepTraffic = false) => {
|
||||||
if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve([] as ApiMsg[]);
|
if (!Array.isArray(emails) || emails.length === 0) return Promise.resolve([] as Msg<unknown>[]);
|
||||||
return removeManyMut.mutateAsync({ emails, keepTraffic });
|
return removeManyMut.mutateAsync({ emails, keepTraffic });
|
||||||
}, [removeManyMut]);
|
}, [removeManyMut]);
|
||||||
const bulkAdjust = useCallback((emails: string[], addDays: number, addBytes: number) => {
|
const bulkAdjust = useCallback((emails: string[], addDays: number, addBytes: number) => {
|
||||||
|
|
@ -305,15 +269,15 @@ export function useClients() {
|
||||||
return bulkAdjustMut.mutateAsync({ emails, addDays, addBytes });
|
return bulkAdjustMut.mutateAsync({ emails, addDays, addBytes });
|
||||||
}, [bulkAdjustMut]);
|
}, [bulkAdjustMut]);
|
||||||
const attach = useCallback((email: string, inboundIds: number[]) => {
|
const attach = useCallback((email: string, inboundIds: number[]) => {
|
||||||
if (!email) return Promise.resolve(null as unknown as ApiMsg);
|
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
return attachMut.mutateAsync({ email, inboundIds });
|
return attachMut.mutateAsync({ email, inboundIds });
|
||||||
}, [attachMut]);
|
}, [attachMut]);
|
||||||
const detach = useCallback((email: string, inboundIds: number[]) => {
|
const detach = useCallback((email: string, inboundIds: number[]) => {
|
||||||
if (!email) return Promise.resolve(null as unknown as ApiMsg);
|
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
return detachMut.mutateAsync({ email, inboundIds });
|
return detachMut.mutateAsync({ email, inboundIds });
|
||||||
}, [detachMut]);
|
}, [detachMut]);
|
||||||
const resetTraffic = useCallback((client: ClientRecord) => {
|
const resetTraffic = useCallback((client: ClientRecord) => {
|
||||||
if (!client?.email) return Promise.resolve(null as unknown as ApiMsg);
|
if (!client?.email) return Promise.resolve(null as unknown as Msg<unknown>);
|
||||||
return resetTrafficMut.mutateAsync(client.email);
|
return resetTrafficMut.mutateAsync(client.email);
|
||||||
}, [resetTrafficMut]);
|
}, [resetTrafficMut]);
|
||||||
const resetAllTraffics = useCallback(() => resetAllTrafficsMut.mutateAsync(), [resetAllTrafficsMut]);
|
const resetAllTraffics = useCallback(() => resetAllTrafficsMut.mutateAsync(), [resetAllTrafficsMut]);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { parseMsg } from '@/utils/zodValidate';
|
||||||
|
import { DefaultsPayloadSchema } from '@/schemas/defaults';
|
||||||
|
|
||||||
type Calendar = 'gregorian' | 'jalalian';
|
type Calendar = 'gregorian' | 'jalalian';
|
||||||
|
|
||||||
|
|
@ -20,12 +22,10 @@ async function loadOnce(): Promise<void> {
|
||||||
}
|
}
|
||||||
pending = (async () => {
|
pending = (async () => {
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings') as {
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||||
success?: boolean;
|
|
||||||
obj?: { datepicker?: Calendar };
|
|
||||||
};
|
|
||||||
if (msg?.success) {
|
if (msg?.success) {
|
||||||
cachedValue = msg.obj?.datepicker || 'gregorian';
|
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||||
|
cachedValue = validated.obj?.datepicker || 'gregorian';
|
||||||
notify(cachedValue);
|
notify(cachedValue);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,25 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { HttpUtil, PromiseUtil } from '@/utils';
|
import { HttpUtil, Msg, PromiseUtil } from '@/utils';
|
||||||
|
import { parseMsg } from '@/utils/zodValidate';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
|
import {
|
||||||
|
OutboundTrafficListSchema,
|
||||||
|
OutboundTestResultSchema,
|
||||||
|
XrayConfigPayloadSchema,
|
||||||
|
XraySettingsValueSchema,
|
||||||
|
type OutboundTestResult,
|
||||||
|
type OutboundTrafficRow,
|
||||||
|
} from '@/schemas/xray';
|
||||||
|
|
||||||
const DIRTY_POLL_MS = 1000;
|
const DIRTY_POLL_MS = 1000;
|
||||||
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
const DEFAULT_TEST_URL = 'https://www.google.com/generate_204';
|
||||||
|
|
||||||
export interface OutboundTrafficRow {
|
export type { OutboundTrafficRow, OutboundTestResult };
|
||||||
tag: string;
|
|
||||||
up: number;
|
|
||||||
down: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutboundTestResult {
|
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||||
success: boolean;
|
|
||||||
delay?: number;
|
|
||||||
error?: string;
|
|
||||||
mode?: string;
|
|
||||||
ttfbMs?: number;
|
|
||||||
tlsMs?: number;
|
|
||||||
connectMs?: number;
|
|
||||||
dnsMs?: number;
|
|
||||||
statusCode?: number;
|
|
||||||
endpoints?: { address: string; delay?: number; success: boolean; error?: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OutboundTestState {
|
export interface OutboundTestState {
|
||||||
testing?: boolean;
|
testing?: boolean;
|
||||||
|
|
@ -32,23 +27,6 @@ export interface OutboundTestState {
|
||||||
mode?: string;
|
mode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface XraySettingsValue {
|
|
||||||
inbounds?: unknown[];
|
|
||||||
outbounds?: { tag?: string; protocol?: string; settings?: unknown; streamSettings?: unknown }[];
|
|
||||||
routing?: {
|
|
||||||
rules?: { type?: string; outboundTag?: string; balancerTag?: string; [key: string]: unknown }[];
|
|
||||||
balancers?: unknown[];
|
|
||||||
domainStrategy?: string;
|
|
||||||
};
|
|
||||||
dns?: { tag?: string; servers?: unknown[] };
|
|
||||||
log?: Record<string, unknown>;
|
|
||||||
policy?: { system?: Record<string, boolean> };
|
|
||||||
observatory?: unknown;
|
|
||||||
burstObservatory?: unknown;
|
|
||||||
fakedns?: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SetTemplate = (
|
export type SetTemplate = (
|
||||||
next: XraySettingsValue | null | ((prev: XraySettingsValue | null) => XraySettingsValue | null),
|
next: XraySettingsValue | null | ((prev: XraySettingsValue | null) => XraySettingsValue | null),
|
||||||
) => void;
|
) => void;
|
||||||
|
|
@ -84,35 +62,32 @@ export interface UseXraySettingResult {
|
||||||
restartXray: () => Promise<void>;
|
restartXray: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
type XrayConfigPayload = z.infer<typeof XrayConfigPayloadSchema>;
|
||||||
success?: boolean;
|
|
||||||
obj?: T;
|
|
||||||
msg?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface XrayConfigPayload {
|
|
||||||
xraySetting: XraySettingsValue;
|
|
||||||
inboundTags?: string[];
|
|
||||||
clientReverseTags?: string[];
|
|
||||||
outboundTestUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchXrayConfig(): Promise<XrayConfigPayload> {
|
async function fetchXrayConfig(): Promise<XrayConfigPayload> {
|
||||||
const msg = await HttpUtil.post('/panel/xray/', undefined, { silent: true }) as ApiMsg<string>;
|
const msg = await HttpUtil.post('/panel/xray/', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load xray config');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load xray config');
|
||||||
if (typeof msg.obj !== 'string') throw new Error('Malformed xray config response: expected string');
|
if (typeof msg.obj !== 'string') throw new Error('Malformed xray config response: expected string');
|
||||||
|
let parsed: unknown;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(msg.obj) as XrayConfigPayload;
|
parsed = JSON.parse(msg.obj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e as Error;
|
const err = e as Error;
|
||||||
throw new Error(`Malformed xray config response: ${err.message}`, { cause: e });
|
throw new Error(`Malformed xray config response: ${err.message}`, { cause: e });
|
||||||
}
|
}
|
||||||
|
const result = XrayConfigPayloadSchema.safeParse(parsed);
|
||||||
|
if (!result.success) {
|
||||||
|
console.warn('[zod] xray/ config payload failed validation', result.error.issues);
|
||||||
|
return parsed as XrayConfigPayload;
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOutboundsTraffic(): Promise<OutboundTrafficRow[]> {
|
async function fetchOutboundsTraffic(): Promise<OutboundTrafficRow[]> {
|
||||||
const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic', undefined, { silent: true }) as ApiMsg<OutboundTrafficRow[]>;
|
const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch outbounds traffic');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch outbounds traffic');
|
||||||
return Array.isArray(msg.obj) ? msg.obj : [];
|
const validated = parseMsg(msg, OutboundTrafficListSchema, 'xray/getOutboundsTraffic');
|
||||||
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useXraySetting(): UseXraySettingResult {
|
export function useXraySetting(): UseXraySettingResult {
|
||||||
|
|
@ -219,7 +194,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
HttpUtil.post('/panel/xray/update', {
|
HttpUtil.post('/panel/xray/update', {
|
||||||
xraySetting: xraySettingRef.current,
|
xraySetting: xraySettingRef.current,
|
||||||
outboundTestUrl: outboundTestUrlRef.current || DEFAULT_TEST_URL,
|
outboundTestUrl: outboundTestUrlRef.current || DEFAULT_TEST_URL,
|
||||||
}) as Promise<ApiMsg>,
|
}),
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.config() });
|
||||||
},
|
},
|
||||||
|
|
@ -227,7 +202,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
|
|
||||||
const resetTrafficMut = useMutation({
|
const resetTrafficMut = useMutation({
|
||||||
mutationFn: (tag: string) =>
|
mutationFn: (tag: string) =>
|
||||||
HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }) as Promise<ApiMsg>,
|
HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }),
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() });
|
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() });
|
||||||
},
|
},
|
||||||
|
|
@ -235,17 +210,21 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
|
|
||||||
const restartMut = useMutation({
|
const restartMut = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const msg = await HttpUtil.post('/panel/api/server/restartXrayService') as ApiMsg;
|
const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||||
if (!msg?.success) return msg;
|
if (!msg?.success) return msg;
|
||||||
await PromiseUtil.sleep(500);
|
await PromiseUtil.sleep(500);
|
||||||
const r = await HttpUtil.get('/panel/xray/getXrayResult') as ApiMsg<string>;
|
const r = await HttpUtil.get('/panel/xray/getXrayResult');
|
||||||
if (r?.success) setRestartResult(r.obj || '');
|
const validated = parseMsg(r, z.string(), 'xray/getXrayResult');
|
||||||
|
if (validated?.success) setRestartResult(validated.obj || '');
|
||||||
return msg;
|
return msg;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetDefaultMut = useMutation({
|
const resetDefaultMut = useMutation({
|
||||||
mutationFn: async () => HttpUtil.get('/panel/setting/getDefaultJsonConfig') as Promise<ApiMsg<XraySettingsValue>>,
|
mutationFn: async (): Promise<Msg<XraySettingsValue>> => {
|
||||||
|
const raw = await HttpUtil.get('/panel/setting/getDefaultJsonConfig');
|
||||||
|
return parseMsg(raw, XraySettingsValueSchema, 'setting/getDefaultJsonConfig');
|
||||||
|
},
|
||||||
onSuccess: (msg) => {
|
onSuccess: (msg) => {
|
||||||
if (msg?.success && msg.obj) {
|
if (msg?.success && msg.obj) {
|
||||||
const cloned = JSON.parse(JSON.stringify(msg.obj));
|
const cloned = JSON.parse(JSON.stringify(msg.obj));
|
||||||
|
|
@ -269,15 +248,16 @@ export function useXraySetting(): UseXraySettingResult {
|
||||||
[index]: { testing: true, result: null, mode },
|
[index]: { testing: true, result: null, mode },
|
||||||
}));
|
}));
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/panel/xray/testOutbound', {
|
const raw = await HttpUtil.post('/panel/xray/testOutbound', {
|
||||||
outbound: JSON.stringify(outbound),
|
outbound: JSON.stringify(outbound),
|
||||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||||
mode,
|
mode,
|
||||||
}) as ApiMsg<OutboundTestResult>;
|
});
|
||||||
|
const msg = parseMsg(raw, OutboundTestResultSchema, 'xray/testOutbound');
|
||||||
if (msg?.success && msg.obj) {
|
if (msg?.success && msg.obj) {
|
||||||
setOutboundTestStates((prev) => ({
|
setOutboundTestStates((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[index]: { testing: false, result: msg.obj as OutboundTestResult },
|
[index]: { testing: false, result: msg.obj },
|
||||||
}));
|
}));
|
||||||
return msg.obj;
|
return msg.obj;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { HttpUtil } from '@/utils';
|
import { HttpUtil } from '@/utils';
|
||||||
|
import { parseMsg } from '@/utils/zodValidate';
|
||||||
import { DBInbound } from '@/models/dbinbound';
|
import { DBInbound } from '@/models/dbinbound';
|
||||||
import { Protocols } from '@/models/inbound';
|
import { Protocols } from '@/models/inbound';
|
||||||
import { setDatepicker } from '@/hooks/useDatepicker';
|
import { setDatepicker } from '@/hooks/useDatepicker';
|
||||||
import { keys } from '@/api/queryKeys';
|
import { keys } from '@/api/queryKeys';
|
||||||
|
import { SlimInboundListSchema, LastOnlineMapSchema, InboundDetailSchema } from '@/schemas/inbound';
|
||||||
|
import { OnlinesSchema } from '@/schemas/client';
|
||||||
|
import { DefaultsPayloadSchema, type DefaultsPayload } from '@/schemas/defaults';
|
||||||
|
|
||||||
export interface SubSettings {
|
export interface SubSettings {
|
||||||
enable: boolean;
|
enable: boolean;
|
||||||
|
|
@ -27,27 +31,6 @@ interface ClientRollup {
|
||||||
comments: Map<string, string>;
|
comments: Map<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
|
||||||
success?: boolean;
|
|
||||||
obj?: T;
|
|
||||||
msg?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DefaultsPayload {
|
|
||||||
expireDiff?: number;
|
|
||||||
trafficDiff?: number;
|
|
||||||
tgBotEnable?: boolean;
|
|
||||||
subEnable?: boolean;
|
|
||||||
subTitle?: string;
|
|
||||||
subURI?: string;
|
|
||||||
subJsonURI?: string;
|
|
||||||
subJsonEnable?: boolean;
|
|
||||||
pageSize?: number;
|
|
||||||
remarkModel?: string;
|
|
||||||
datepicker?: string;
|
|
||||||
ipLimitEnable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRACKED_PROTOCOLS = [
|
const TRACKED_PROTOCOLS = [
|
||||||
Protocols.VMESS,
|
Protocols.VMESS,
|
||||||
Protocols.VLESS,
|
Protocols.VLESS,
|
||||||
|
|
@ -57,27 +40,31 @@ const TRACKED_PROTOCOLS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
async function fetchSlimInbounds(): Promise<unknown[]> {
|
async function fetchSlimInbounds(): Promise<unknown[]> {
|
||||||
const msg = await HttpUtil.get('/panel/api/inbounds/list/slim', undefined, { silent: true }) as ApiMsg<unknown[]>;
|
const msg = await HttpUtil.get('/panel/api/inbounds/list/slim', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch inbounds');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch inbounds');
|
||||||
return Array.isArray(msg.obj) ? msg.obj : [];
|
const validated = parseMsg(msg, SlimInboundListSchema, 'inbounds/list/slim');
|
||||||
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchOnlineClients(): Promise<string[]> {
|
async function fetchOnlineClients(): Promise<string[]> {
|
||||||
const msg = await HttpUtil.post('/panel/api/clients/onlines', undefined, { silent: true }) as ApiMsg<string[]>;
|
const msg = await HttpUtil.post('/panel/api/clients/onlines', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch onlines');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch onlines');
|
||||||
return Array.isArray(msg.obj) ? msg.obj : [];
|
const validated = parseMsg(msg, OnlinesSchema, 'clients/onlines');
|
||||||
|
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchLastOnlineMap(): Promise<Record<string, number>> {
|
async function fetchLastOnlineMap(): Promise<Record<string, number>> {
|
||||||
const msg = await HttpUtil.post('/panel/api/clients/lastOnline', undefined, { silent: true }) as ApiMsg<Record<string, number>>;
|
const msg = await HttpUtil.post('/panel/api/clients/lastOnline', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch lastOnline');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch lastOnline');
|
||||||
return (msg.obj && typeof msg.obj === 'object') ? msg.obj : {};
|
const validated = parseMsg(msg, LastOnlineMapSchema, 'clients/lastOnline');
|
||||||
|
return (validated.obj && typeof validated.obj === 'object') ? validated.obj : {};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchDefaultSettings(): Promise<DefaultsPayload> {
|
async function fetchDefaultSettings(): Promise<DefaultsPayload> {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true }) as ApiMsg<DefaultsPayload>;
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true });
|
||||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
||||||
return (msg.obj as DefaultsPayload) || {};
|
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||||
|
return validated.obj ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInbounds() {
|
export function useInbounds() {
|
||||||
|
|
@ -272,8 +259,9 @@ export function useInbounds() {
|
||||||
const hydrateInbound = useCallback(async (id: number) => {
|
const hydrateInbound = useCallback(async (id: number) => {
|
||||||
const msg = await HttpUtil.get(`/panel/api/inbounds/get/${id}`);
|
const msg = await HttpUtil.get(`/panel/api/inbounds/get/${id}`);
|
||||||
if (!msg?.success || !msg.obj) return null;
|
if (!msg?.success || !msg.obj) return null;
|
||||||
const full = msg.obj as { id: number; protocol: string };
|
const validated = parseMsg(msg, InboundDetailSchema, `inbounds/get/${id}`);
|
||||||
const dbInbound = new DBInbound(full) as DBInboundInstance;
|
if (!validated.obj) return null;
|
||||||
|
const dbInbound = new DBInbound(validated.obj) as DBInboundInstance;
|
||||||
setDbInbounds((prev) => {
|
setDbInbounds((prev) => {
|
||||||
const next = prev.map((row) => (
|
const next = prev.map((row) => (
|
||||||
(row as unknown as { id: number }).id === id ? dbInbound : row
|
(row as unknown as { id: number }).id === id ? dbInbound : row
|
||||||
|
|
|
||||||
|
|
@ -14,27 +14,18 @@ import {
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
import type { Msg } from '@/utils';
|
||||||
|
import type { ProbeResult } from '@/schemas/node';
|
||||||
import './NodeFormModal.css';
|
import './NodeFormModal.css';
|
||||||
|
|
||||||
type Mode = 'add' | 'edit';
|
type Mode = 'add' | 'edit';
|
||||||
|
|
||||||
interface ApiMsg<T = unknown> {
|
|
||||||
success?: boolean;
|
|
||||||
msg?: string;
|
|
||||||
obj?: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeFormModalProps {
|
interface NodeFormModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
node: NodeRecord | null;
|
node: NodeRecord | null;
|
||||||
testConnection: (payload: Partial<NodeRecord>) => Promise<ApiMsg<{
|
testConnection: (payload: Partial<NodeRecord>) => Promise<Msg<ProbeResult>>;
|
||||||
status: string;
|
save: (payload: Partial<NodeRecord>) => Promise<Msg<unknown>>;
|
||||||
latencyMs?: number;
|
|
||||||
xrayVersion?: string;
|
|
||||||
error?: string;
|
|
||||||
}>>;
|
|
||||||
save: (payload: Partial<NodeRecord>) => Promise<ApiMsg>;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ export default function OutboundsTab({
|
||||||
const [existingTags, setExistingTags] = useState<string[]>([]);
|
const [existingTags, setExistingTags] = useState<string[]>([]);
|
||||||
|
|
||||||
const outbounds = useMemo(
|
const outbounds = useMemo(
|
||||||
() => (templateSettings?.outbounds || []) as OutboundRow[],
|
() => (templateSettings?.outbounds || []) as unknown as OutboundRow[],
|
||||||
[templateSettings?.outbounds],
|
[templateSettings?.outbounds],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
84
frontend/src/schemas/client.ts
Normal file
84
frontend/src/schemas/client.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const ClientTrafficSchema = z.object({
|
||||||
|
up: z.number().optional(),
|
||||||
|
down: z.number().optional(),
|
||||||
|
total: z.number().optional(),
|
||||||
|
expiryTime: z.number().optional(),
|
||||||
|
enable: z.boolean().optional(),
|
||||||
|
lastOnline: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ClientRecordSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
subId: z.string().optional(),
|
||||||
|
uuid: z.string().optional(),
|
||||||
|
password: z.string().optional(),
|
||||||
|
auth: z.string().optional(),
|
||||||
|
flow: z.string().optional(),
|
||||||
|
totalGB: z.number().optional(),
|
||||||
|
expiryTime: z.number().optional(),
|
||||||
|
limitIp: z.number().optional(),
|
||||||
|
tgId: z.union([z.number(), z.string()]).optional(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
enable: z.boolean().optional(),
|
||||||
|
inboundIds: z.array(z.number()).optional(),
|
||||||
|
traffic: ClientTrafficSchema.optional(),
|
||||||
|
reverse: z.object({ tag: z.string().optional() }).loose().optional(),
|
||||||
|
createdAt: z.number().optional(),
|
||||||
|
updatedAt: z.number().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const InboundOptionSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
remark: z.string().optional(),
|
||||||
|
protocol: z.string().optional(),
|
||||||
|
port: z.number().optional(),
|
||||||
|
tlsFlowCapable: z.boolean().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const InboundOptionsSchema = z.array(InboundOptionSchema);
|
||||||
|
|
||||||
|
export const ClientsSummarySchema = z.object({
|
||||||
|
total: z.number(),
|
||||||
|
active: z.number(),
|
||||||
|
online: z.array(z.string()),
|
||||||
|
depleted: z.array(z.string()),
|
||||||
|
expiring: z.array(z.string()),
|
||||||
|
deactive: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ClientPageResponseSchema = z.object({
|
||||||
|
items: z.array(ClientRecordSchema),
|
||||||
|
total: z.number(),
|
||||||
|
filtered: z.number(),
|
||||||
|
page: z.number(),
|
||||||
|
pageSize: z.number(),
|
||||||
|
summary: ClientsSummarySchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ClientHydrateSchema = z.object({
|
||||||
|
client: ClientRecordSchema,
|
||||||
|
inboundIds: z.array(z.number()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BulkAdjustResultSchema = z.object({
|
||||||
|
adjusted: z.number(),
|
||||||
|
skipped: z
|
||||||
|
.array(z.object({ email: z.string(), reason: z.string() }))
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DelDepletedResultSchema = z.object({
|
||||||
|
deleted: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OnlinesSchema = z.array(z.string());
|
||||||
|
|
||||||
|
export type ClientRecord = z.infer<typeof ClientRecordSchema>;
|
||||||
|
export type ClientTraffic = z.infer<typeof ClientTrafficSchema>;
|
||||||
|
export type InboundOption = z.infer<typeof InboundOptionSchema>;
|
||||||
|
export type ClientsSummary = z.infer<typeof ClientsSummarySchema>;
|
||||||
|
export type ClientPageResponse = z.infer<typeof ClientPageResponseSchema>;
|
||||||
|
export type ClientHydrate = z.infer<typeof ClientHydrateSchema>;
|
||||||
|
export type BulkAdjustResult = z.infer<typeof BulkAdjustResultSchema>;
|
||||||
18
frontend/src/schemas/defaults.ts
Normal file
18
frontend/src/schemas/defaults.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const DefaultsPayloadSchema = z.object({
|
||||||
|
expireDiff: z.number().optional(),
|
||||||
|
trafficDiff: z.number().optional(),
|
||||||
|
tgBotEnable: z.boolean().optional(),
|
||||||
|
subEnable: z.boolean().optional(),
|
||||||
|
subTitle: z.string().optional(),
|
||||||
|
subURI: z.string().optional(),
|
||||||
|
subJsonURI: z.string().optional(),
|
||||||
|
subJsonEnable: z.boolean().optional(),
|
||||||
|
pageSize: z.number().optional(),
|
||||||
|
remarkModel: z.string().optional(),
|
||||||
|
datepicker: z.enum(['gregorian', 'jalalian']).optional(),
|
||||||
|
ipLimitEnable: z.boolean().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export type DefaultsPayload = z.infer<typeof DefaultsPayloadSchema>;
|
||||||
19
frontend/src/schemas/inbound.ts
Normal file
19
frontend/src/schemas/inbound.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const SlimInboundSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
protocol: z.string(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const SlimInboundListSchema = z.array(SlimInboundSchema);
|
||||||
|
|
||||||
|
export const InboundDetailSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
protocol: z.string(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const LastOnlineMapSchema = z.record(z.string(), z.number());
|
||||||
|
|
||||||
|
export type SlimInbound = z.infer<typeof SlimInboundSchema>;
|
||||||
|
export type InboundDetail = z.infer<typeof InboundDetailSchema>;
|
||||||
|
export type LastOnlineMap = z.infer<typeof LastOnlineMapSchema>;
|
||||||
|
|
@ -28,4 +28,12 @@ export const NodeRecordSchema = z.object({
|
||||||
|
|
||||||
export const NodeListSchema = z.array(NodeRecordSchema);
|
export const NodeListSchema = z.array(NodeRecordSchema);
|
||||||
|
|
||||||
|
export const ProbeResultSchema = z.object({
|
||||||
|
status: z.string(),
|
||||||
|
latencyMs: z.number().optional(),
|
||||||
|
xrayVersion: z.string().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
export type NodeRecord = z.infer<typeof NodeRecordSchema>;
|
export type NodeRecord = z.infer<typeof NodeRecordSchema>;
|
||||||
|
export type ProbeResult = z.infer<typeof ProbeResultSchema>;
|
||||||
|
|
|
||||||
77
frontend/src/schemas/xray.ts
Normal file
77
frontend/src/schemas/xray.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const XraySettingsValueSchema = z.object({
|
||||||
|
inbounds: z.array(z.unknown()).optional(),
|
||||||
|
outbounds: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
protocol: z.string().optional(),
|
||||||
|
settings: z.unknown().optional(),
|
||||||
|
streamSettings: z.unknown().optional(),
|
||||||
|
}).loose(),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
routing: z.object({
|
||||||
|
rules: z.array(z.object({
|
||||||
|
type: z.string().optional(),
|
||||||
|
outboundTag: z.string().optional(),
|
||||||
|
balancerTag: z.string().optional(),
|
||||||
|
}).loose()).optional(),
|
||||||
|
balancers: z.array(z.unknown()).optional(),
|
||||||
|
domainStrategy: z.string().optional(),
|
||||||
|
}).loose().optional(),
|
||||||
|
dns: z.object({
|
||||||
|
tag: z.string().optional(),
|
||||||
|
servers: z.array(z.unknown()).optional(),
|
||||||
|
}).loose().optional(),
|
||||||
|
log: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
policy: z.object({
|
||||||
|
system: z.record(z.string(), z.boolean()).optional(),
|
||||||
|
}).loose().optional(),
|
||||||
|
observatory: z.unknown().optional(),
|
||||||
|
burstObservatory: z.unknown().optional(),
|
||||||
|
fakedns: z.unknown().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const XrayConfigPayloadSchema = z.object({
|
||||||
|
xraySetting: XraySettingsValueSchema,
|
||||||
|
inboundTags: z.array(z.string()).optional(),
|
||||||
|
clientReverseTags: z.array(z.string()).optional(),
|
||||||
|
outboundTestUrl: z.string().optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export const OutboundTrafficRowSchema = z.object({
|
||||||
|
tag: z.string(),
|
||||||
|
up: z.number(),
|
||||||
|
down: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OutboundTrafficListSchema = z.array(OutboundTrafficRowSchema);
|
||||||
|
|
||||||
|
export const OutboundTestResultSchema = z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
delay: z.number().optional(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
mode: z.string().optional(),
|
||||||
|
ttfbMs: z.number().optional(),
|
||||||
|
tlsMs: z.number().optional(),
|
||||||
|
connectMs: z.number().optional(),
|
||||||
|
dnsMs: z.number().optional(),
|
||||||
|
statusCode: z.number().optional(),
|
||||||
|
endpoints: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
address: z.string(),
|
||||||
|
delay: z.number().optional(),
|
||||||
|
success: z.boolean(),
|
||||||
|
error: z.string().optional(),
|
||||||
|
}).loose(),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}).loose();
|
||||||
|
|
||||||
|
export type XraySettingsValue = z.infer<typeof XraySettingsValueSchema>;
|
||||||
|
export type XrayConfigPayload = z.infer<typeof XrayConfigPayloadSchema>;
|
||||||
|
export type OutboundTrafficRow = z.infer<typeof OutboundTrafficRowSchema>;
|
||||||
|
export type OutboundTestResult = z.infer<typeof OutboundTestResultSchema>;
|
||||||
Loading…
Reference in a new issue