mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-08 05:14:33 +00:00
refactor(api)!: move /panel/setting and /panel/xray under /panel/api
Settings and Xray config endpoints now live at /panel/api/setting/* and /panel/api/xray/*, registered under the existing /panel/api group so they inherit the same Bearer-or-session auth (checkAPIAuth) as the rest of the API. An API token is a full-admin credential, so this just makes the surface consistent. The SPA page routes /panel/settings and /panel/xray are unchanged. BREAKING CHANGE: the old /panel/setting/* and /panel/xray/* paths are removed. External callers must switch to the /panel/api/ prefix. Frontend call sites, API docs, the dev proxy, and the route-documentation test are updated to match.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ import { AllSettingSchema, type AllSettingInput } from '@/schemas/setting';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
|
||||
async function fetchAllSetting(): Promise<AllSettingInput | null> {
|
||||
const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
|
||||
const msg = await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch settings');
|
||||
const validated = parseMsg(msg, AllSettingSchema, 'setting/all');
|
||||
return validated.obj;
|
||||
@@ -47,7 +47,7 @@ export function useAllSettings() {
|
||||
if (!body.success) {
|
||||
console.warn('[zod] setting/update body failed validation', body.error.issues);
|
||||
}
|
||||
return HttpUtil.post('/panel/setting/update', body.success ? body.data : next);
|
||||
return HttpUtil.post('/panel/api/setting/update', body.success ? body.data : next);
|
||||
},
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.settings.all() });
|
||||
|
||||
@@ -142,7 +142,7 @@ async function fetchInboundOptions(): Promise<InboundOption[]> {
|
||||
}
|
||||
|
||||
async function fetchDefaults(): Promise<Record<string, unknown>> {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true });
|
||||
const msg = await HttpUtil.post('/panel/api/setting/defaultSettings', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
||||
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||
return validated.obj || {};
|
||||
|
||||
@@ -22,7 +22,7 @@ async function loadOnce(): Promise<void> {
|
||||
}
|
||||
pending = (async () => {
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||
const msg = await HttpUtil.post('/panel/api/setting/defaultSettings');
|
||||
if (msg?.success) {
|
||||
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||
cachedValue = validated.obj?.datepicker || 'gregorian';
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface UseXraySettingResult {
|
||||
type XrayConfigPayload = z.infer<typeof XrayConfigPayloadSchema>;
|
||||
|
||||
async function fetchXrayConfig(): Promise<XrayConfigPayload> {
|
||||
const msg = await HttpUtil.post('/panel/xray/', undefined, { silent: true });
|
||||
const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true });
|
||||
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');
|
||||
let parsed: unknown;
|
||||
@@ -91,7 +91,7 @@ async function fetchXrayConfig(): Promise<XrayConfigPayload> {
|
||||
}
|
||||
|
||||
async function fetchOutboundsTraffic(): Promise<OutboundTrafficRow[]> {
|
||||
const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic', undefined, { silent: true });
|
||||
const msg = await HttpUtil.get('/panel/api/xray/getOutboundsTraffic', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch outbounds traffic');
|
||||
const validated = parseMsg(msg, OutboundTrafficListSchema, 'xray/getOutboundsTraffic');
|
||||
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||
@@ -200,7 +200,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
mutationFn: async () => {
|
||||
const sentXraySetting = xraySettingRef.current;
|
||||
const sentTestUrl = outboundTestUrlRef.current || DEFAULT_TEST_URL;
|
||||
const msg = await HttpUtil.post('/panel/xray/update', {
|
||||
const msg = await HttpUtil.post('/panel/api/xray/update', {
|
||||
xraySetting: sentXraySetting,
|
||||
outboundTestUrl: sentTestUrl,
|
||||
});
|
||||
@@ -217,7 +217,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const resetTrafficMut = useMutation({
|
||||
mutationFn: (tag: string) =>
|
||||
HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }),
|
||||
HttpUtil.post('/panel/api/xray/resetOutboundsTraffic', { tag }),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() });
|
||||
},
|
||||
@@ -228,7 +228,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||
if (!msg?.success) return msg;
|
||||
await PromiseUtil.sleep(500);
|
||||
const r = await HttpUtil.get('/panel/xray/getXrayResult');
|
||||
const r = await HttpUtil.get('/panel/api/xray/getXrayResult');
|
||||
const validated = parseMsg(r, z.string(), 'xray/getXrayResult');
|
||||
if (validated?.success) setRestartResult(validated.obj || '');
|
||||
return msg;
|
||||
@@ -237,7 +237,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
|
||||
const resetDefaultMut = useMutation({
|
||||
mutationFn: async (): Promise<Msg<XraySettingsValue>> => {
|
||||
const raw = await HttpUtil.get('/panel/setting/getDefaultJsonConfig');
|
||||
const raw = await HttpUtil.get('/panel/api/setting/getDefaultJsonConfig');
|
||||
return parseMsg(raw, XraySettingsValueSchema, 'setting/getDefaultJsonConfig');
|
||||
},
|
||||
onSuccess: (msg) => {
|
||||
@@ -264,7 +264,7 @@ export function useXraySetting(): UseXraySettingResult {
|
||||
[index]: { testing: true, result: null, mode: effMode },
|
||||
}));
|
||||
try {
|
||||
const raw = await HttpUtil.post('/panel/xray/testOutbound', {
|
||||
const raw = await HttpUtil.post('/panel/api/xray/testOutbound', {
|
||||
outbound: JSON.stringify(outbound),
|
||||
allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []),
|
||||
mode: effMode,
|
||||
|
||||
@@ -917,28 +917,28 @@ export const sections: readonly Section[] = [
|
||||
id: 'settings',
|
||||
title: 'Settings',
|
||||
description:
|
||||
'Panel configuration and user credentials. All endpoints live under /panel/setting and require a logged-in session or Bearer token.',
|
||||
'Panel configuration and user credentials. All endpoints live under /panel/api/setting and require a logged-in session or Bearer token.',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/all',
|
||||
path: '/panel/api/setting/all',
|
||||
summary: 'Return every panel setting: web server, Telegram bot, subscription, security, LDAP. The full JSON blob that the Settings page edits.',
|
||||
response: '{\n "success": true,\n "obj": {\n "webPort": 2053,\n "webCertFile": "",\n "webKeyFile": "",\n "webBasePath": "/",\n "subPort": 10882,\n "subPath": "/sub/",\n "tgBotEnable": false,\n "tgBotToken": "",\n ...\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/defaultSettings',
|
||||
path: '/panel/api/setting/defaultSettings',
|
||||
summary: 'Return the computed default settings based on the request host. Useful to preview what a fresh install would use.',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/update',
|
||||
path: '/panel/api/setting/update',
|
||||
summary: 'Persist every setting at once. The body mirrors the shape returned by /all. Invalid values (bad ports, missing cert pairs, etc.) are rejected before write.',
|
||||
body: '{\n "webPort": 2053,\n "webBasePath": "/",\n "subPort": 10882,\n "subPath": "/sub/",\n "tgBotEnable": false,\n ...\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/updateUser',
|
||||
path: '/panel/api/setting/updateUser',
|
||||
summary: 'Change the panel admin username and password. Requires the current credentials for verification. The session is refreshed with the new values on success.',
|
||||
params: [
|
||||
{ name: 'oldUsername', in: 'body', type: 'string', desc: 'Current admin username.' },
|
||||
@@ -950,12 +950,12 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/restartPanel',
|
||||
path: '/panel/api/setting/restartPanel',
|
||||
summary: 'Restart the entire 3x-ui process after a 3-second grace period. The connection drops immediately; the panel comes back online ~5-10 seconds later.',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/setting/getDefaultJsonConfig',
|
||||
path: '/panel/api/setting/getDefaultJsonConfig',
|
||||
summary: 'Return the built-in default Xray JSON config template that ships with this panel version.',
|
||||
},
|
||||
],
|
||||
@@ -965,17 +965,17 @@ export const sections: readonly Section[] = [
|
||||
id: 'api-tokens',
|
||||
title: 'API Tokens',
|
||||
description:
|
||||
'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request.',
|
||||
'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as <code>Authorization: Bearer <token></code> on any /panel/api/* request — the token is a full-admin credential.',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/setting/apiTokens',
|
||||
path: '/panel/api/setting/apiTokens',
|
||||
summary: 'List every API token, enabled or not. The token value is never returned — only metadata.',
|
||||
response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/apiTokens/create',
|
||||
path: '/panel/api/setting/apiTokens/create',
|
||||
summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.',
|
||||
params: [
|
||||
{ name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' },
|
||||
@@ -986,7 +986,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/apiTokens/delete/:id',
|
||||
path: '/panel/api/setting/apiTokens/delete/:id',
|
||||
summary: 'Permanently delete a token. Any caller using it stops authenticating immediately.',
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Token row ID.' },
|
||||
@@ -995,7 +995,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/setting/apiTokens/setEnabled/:id',
|
||||
path: '/panel/api/setting/apiTokens/setEnabled/:id',
|
||||
summary: 'Toggle a token enabled/disabled without deleting it. Disabled tokens are rejected by checkAPIAuth on the next request.',
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Token row ID.' },
|
||||
@@ -1011,32 +1011,32 @@ export const sections: readonly Section[] = [
|
||||
id: 'xray-settings',
|
||||
title: 'Xray Settings',
|
||||
description:
|
||||
'Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/xray.',
|
||||
'Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/api/xray.',
|
||||
endpoints: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/',
|
||||
path: '/panel/api/xray/',
|
||||
summary: 'Return the Xray config template (JSON string), available inbound tags, client reverse tags, and the configured outbound test URL in one response.',
|
||||
response: '{\n "success": true,\n "obj": {\n "xraySetting": "{...raw xray config...}",\n "inboundTags": "[\\"in-443-tcp\\"]",\n "clientReverseTags": "[]",\n "outboundTestUrl": "https://www.google.com/generate_204"\n }\n}',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/xray/getDefaultJsonConfig',
|
||||
summary: 'Return the built-in default Xray config shipped with the panel (identical to /panel/setting/getDefaultJsonConfig).',
|
||||
path: '/panel/api/xray/getDefaultJsonConfig',
|
||||
summary: 'Return the built-in default Xray config shipped with the panel (identical to /panel/api/setting/getDefaultJsonConfig).',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/xray/getOutboundsTraffic',
|
||||
path: '/panel/api/xray/getOutboundsTraffic',
|
||||
summary: 'Return traffic statistics for every outbound. Each outbound shows up/down/total counters.',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/xray/getXrayResult',
|
||||
path: '/panel/api/xray/getXrayResult',
|
||||
summary: 'Return the most recent Xray process stdout/stderr output. Useful to check for startup errors or runtime warnings.',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/update',
|
||||
path: '/panel/api/xray/update',
|
||||
summary: 'Save the Xray JSON config template and optionally the outbound test URL. Both are sent as form fields.',
|
||||
params: [
|
||||
{ name: 'xraySetting', in: 'body (form)', type: 'string', desc: 'Full Xray JSON config template.' },
|
||||
@@ -1045,7 +1045,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/warp/:action',
|
||||
path: '/panel/api/xray/warp/:action',
|
||||
summary: 'Manage Cloudflare Warp integration. The action parameter selects the operation.',
|
||||
params: [
|
||||
{ name: 'action', in: 'path', type: 'string', desc: 'data — return Warp stats (quota, remaining). del — delete Warp data. config — return current Warp config. reg — register a new Warp endpoint (sends privateKey, publicKey). license — set a Warp+ license key (sends license).' },
|
||||
@@ -1056,7 +1056,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/nord/:action',
|
||||
path: '/panel/api/xray/nord/:action',
|
||||
summary: 'Manage NordVPN integration. The action parameter selects the operation.',
|
||||
params: [
|
||||
{ name: 'action', in: 'path', type: 'string', desc: 'countries — list available countries. servers — list servers in a country (sends countryId). reg — get NordVPN credentials (sends token). setKey — store NordVPN API key (sends key). data — return current NordVPN connection data. del — delete NordVPN data.' },
|
||||
@@ -1067,7 +1067,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/resetOutboundsTraffic',
|
||||
path: '/panel/api/xray/resetOutboundsTraffic',
|
||||
summary: 'Reset traffic counters for a specific outbound by tag.',
|
||||
params: [
|
||||
{ name: 'tag', in: 'body (form)', type: 'string', desc: 'Outbound tag to reset (e.g. "proxy", "direct").' },
|
||||
@@ -1076,7 +1076,7 @@ export const sections: readonly Section[] = [
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/xray/testOutbound',
|
||||
path: '/panel/api/xray/testOutbound',
|
||||
summary: 'Test an outbound configuration. Sends the outbound JSON (required), optionally all outbounds (to resolve sockopt.dialerProxy dependencies), and a mode flag.',
|
||||
params: [
|
||||
{ name: 'outbound', in: 'body (form)', type: 'string', desc: 'JSON-encoded single outbound to test (required).' },
|
||||
|
||||
@@ -120,7 +120,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
|
||||
// node's own paths (fetched through the central panel), not this panel's.
|
||||
const msg = typeof nodeId === 'number'
|
||||
? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
|
||||
: await HttpUtil.post('/panel/setting/all', undefined, { silent: true });
|
||||
: await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
|
||||
if (!msg?.success) {
|
||||
messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
|
||||
return;
|
||||
|
||||
@@ -97,7 +97,7 @@ async function fetchLastOnlineMap(): Promise<Record<string, number>> {
|
||||
}
|
||||
|
||||
async function fetchDefaultSettings(): Promise<DefaultsPayload> {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true });
|
||||
const msg = await HttpUtil.post('/panel/api/setting/defaultSettings', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults');
|
||||
const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings');
|
||||
return validated.obj ?? {};
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function BackupModal({ open, basePath: _basePath, onClose, onBusy
|
||||
}
|
||||
|
||||
onBusy({ busy: true, tip: `${t('pages.settings.restartPanel')}…` });
|
||||
const restart = await HttpUtil.post('/panel/setting/restartPanel');
|
||||
const restart = await HttpUtil.post('/panel/api/setting/restartPanel');
|
||||
if (restart?.success) {
|
||||
await PromiseUtil.sleep(5000);
|
||||
window.location.reload();
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function IndexPage() {
|
||||
const [loadingTip, setLoadingTip] = useState(t('loading'));
|
||||
|
||||
useEffect(() => {
|
||||
HttpUtil.post<{ ipLimitEnable?: boolean }>('/panel/setting/defaultSettings').then((msg) => {
|
||||
HttpUtil.post<{ ipLimitEnable?: boolean }>('/panel/api/setting/defaultSettings').then((msg) => {
|
||||
if (msg?.success && msg.obj) setIpLimitEnable(!!msg.obj.ipLimitEnable);
|
||||
});
|
||||
HttpUtil.get<PanelUpdateInfo>('/panel/api/server/getPanelUpdateInfo').then((msg) => {
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
||||
const sendUpdateUser = useCallback(async () => {
|
||||
setUpdating(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/setting/updateUser', user) as ApiMsg;
|
||||
const msg = await HttpUtil.post('/panel/api/setting/updateUser', user) as ApiMsg;
|
||||
if (msg?.success) {
|
||||
await HttpUtil.post('/logout');
|
||||
const basePath = window.X_UI_BASE_PATH || '/';
|
||||
@@ -124,7 +124,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
||||
const loadApiTokens = useCallback(async () => {
|
||||
setApiTokensLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/setting/apiTokens') as ApiMsg<ApiTokenRow[]>;
|
||||
const msg = await HttpUtil.get('/panel/api/setting/apiTokens') as ApiMsg<ApiTokenRow[]>;
|
||||
if (msg?.success) setApiTokens(Array.isArray(msg.obj) ? msg.obj : []);
|
||||
} finally {
|
||||
setApiTokensLoading(false);
|
||||
@@ -156,7 +156,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
||||
}
|
||||
setCreating(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>;
|
||||
const msg = await HttpUtil.post('/panel/api/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>;
|
||||
if (msg?.success) {
|
||||
setCreateOpen(false);
|
||||
await loadApiTokens();
|
||||
@@ -178,7 +178,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
||||
cancelText: t('cancel'),
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
const msg = await HttpUtil.post(`/panel/setting/apiTokens/delete/${row.id}`) as ApiMsg;
|
||||
const msg = await HttpUtil.post(`/panel/api/setting/apiTokens/delete/${row.id}`) as ApiMsg;
|
||||
if (msg?.success) await loadApiTokens();
|
||||
},
|
||||
});
|
||||
@@ -186,7 +186,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr
|
||||
|
||||
async function toggleTokenEnabled(row: ApiTokenRow) {
|
||||
const target = !row.enabled;
|
||||
const msg = await HttpUtil.post(`/panel/setting/apiTokens/setEnabled/${row.id}`, { enabled: target }) as ApiMsg;
|
||||
const msg = await HttpUtil.post(`/panel/api/setting/apiTokens/setEnabled/${row.id}`, { enabled: target }) as ApiMsg;
|
||||
if (msg?.success) {
|
||||
setApiTokens((prev) => prev.map((r) => (r.id === row.id ? { ...r, enabled: target } : r)));
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ export default function SettingsPage() {
|
||||
onOk: async () => {
|
||||
setSpinning(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/setting/restartPanel') as ApiMsg;
|
||||
const msg = await HttpUtil.post('/panel/api/setting/restartPanel') as ApiMsg;
|
||||
if (!msg?.success) return;
|
||||
await PromiseUtil.sleep(5000);
|
||||
window.location.replace(rebuildUrlAfterRestart());
|
||||
|
||||
@@ -88,14 +88,14 @@ export default function NordModal({
|
||||
}, [filteredServers]);
|
||||
|
||||
const fetchCountries = useCallback(async () => {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/nord/countries');
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/nord/countries');
|
||||
if (msg?.success && msg.obj) setCountries(JSON.parse(msg.obj));
|
||||
}, []);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/nord/data');
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/nord/data');
|
||||
if (msg?.success) {
|
||||
const next = msg.obj ? JSON.parse(msg.obj) : null;
|
||||
setNordData(next);
|
||||
@@ -113,7 +113,7 @@ export default function NordModal({
|
||||
async function login() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/nord/reg', { token });
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/nord/reg', { token });
|
||||
if (msg?.success && msg.obj) {
|
||||
setNordData(JSON.parse(msg.obj));
|
||||
await fetchCountries();
|
||||
@@ -126,7 +126,7 @@ export default function NordModal({
|
||||
async function saveKey() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/nord/setKey', { key: manualKey });
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/nord/setKey', { key: manualKey });
|
||||
if (msg?.success && msg.obj) {
|
||||
setNordData(JSON.parse(msg.obj));
|
||||
await fetchCountries();
|
||||
@@ -139,7 +139,7 @@ export default function NordModal({
|
||||
async function logout() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/xray/nord/del');
|
||||
const msg = await HttpUtil.post('/panel/api/xray/nord/del');
|
||||
if (msg?.success) {
|
||||
onRemoveOutbound(nordOutboundIndex);
|
||||
onRemoveRoutingRules({ prefix: 'nord-' });
|
||||
@@ -166,7 +166,7 @@ export default function NordModal({
|
||||
setServerId(null);
|
||||
setCityId(null);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/nord/servers', { countryId: newCountryId });
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/nord/servers', { countryId: newCountryId });
|
||||
if (!msg?.success || !msg.obj) return;
|
||||
const data = JSON.parse(msg.obj);
|
||||
const locations = data.locations || [];
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function WarpModal({
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/warp/data');
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/warp/data');
|
||||
if (msg?.success) {
|
||||
const raw = msg.obj;
|
||||
setWarpData(raw && raw.length > 0 ? JSON.parse(raw) : null);
|
||||
@@ -133,7 +133,7 @@ export default function WarpModal({
|
||||
setLoading(true);
|
||||
try {
|
||||
const keys = Wireguard.generateKeypair();
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/warp/reg', keys);
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/warp/reg', keys);
|
||||
if (msg?.success && msg.obj) {
|
||||
const resp = JSON.parse(msg.obj);
|
||||
setWarpData(resp.data);
|
||||
@@ -148,7 +148,7 @@ export default function WarpModal({
|
||||
async function getConfig() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/warp/config');
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/warp/config');
|
||||
if (msg?.success && msg.obj) {
|
||||
const parsed = JSON.parse(msg.obj);
|
||||
setWarpConfig(parsed);
|
||||
@@ -164,7 +164,7 @@ export default function WarpModal({
|
||||
setLoading(true);
|
||||
setLicenseError('');
|
||||
try {
|
||||
const msg = await HttpUtil.post<string>('/panel/xray/warp/license', { license: warpPlus });
|
||||
const msg = await HttpUtil.post<string>('/panel/api/xray/warp/license', { license: warpPlus });
|
||||
if (msg?.success && msg.obj) {
|
||||
setWarpData(JSON.parse(msg.obj));
|
||||
setWarpConfig(null);
|
||||
@@ -180,7 +180,7 @@ export default function WarpModal({
|
||||
async function delConfig() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post('/panel/xray/warp/del');
|
||||
const msg = await HttpUtil.post('/panel/api/xray/warp/del');
|
||||
if (msg?.success) {
|
||||
setWarpData(null);
|
||||
setWarpConfig(null);
|
||||
|
||||
@@ -22,7 +22,7 @@ function resolveDBPath() {
|
||||
return '/etc/x-ui/x-ui.db';
|
||||
}
|
||||
|
||||
const PANEL_API_PREFIXES = ['panel/api/', 'panel/setting/', 'panel/xray/', 'panel/csrf-token'];
|
||||
const PANEL_API_PREFIXES = ['panel/api/', 'panel/csrf-token'];
|
||||
|
||||
let cachedBasePath = '/';
|
||||
|
||||
|
||||
@@ -14,13 +14,15 @@ import (
|
||||
// APIController handles the main API routes for the 3x-ui panel, including inbounds and server management.
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
nodeController *NodeController
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
apiTokenService service.ApiTokenService
|
||||
Tgbot service.Tgbot
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
nodeController *NodeController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
settingService service.SettingService
|
||||
userService service.UserService
|
||||
apiTokenService service.ApiTokenService
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
// NewAPIController creates a new APIController instance and initializes its routes.
|
||||
@@ -79,6 +81,12 @@ func (a *APIController) initRouter(g *gin.RouterGroup, customGeo *service.Custom
|
||||
|
||||
NewCustomGeoController(api.Group("/custom-geo"), customGeo)
|
||||
|
||||
// Settings + Xray config management live under the API surface too, so the
|
||||
// same API token drives them. Paths are /panel/api/setting/* and
|
||||
// /panel/api/xray/*.
|
||||
a.settingController = NewSettingController(api)
|
||||
a.xraySettingController = NewXraySettingController(api)
|
||||
|
||||
// Extra routes
|
||||
api.POST("/backuptotgbot", a.BackuptoTgbot)
|
||||
}
|
||||
|
||||
@@ -96,9 +96,9 @@ func TestAPIRoutesDocumented(t *testing.T) {
|
||||
case "node.go":
|
||||
basePath = "/panel/api/nodes"
|
||||
case "setting.go":
|
||||
basePath = "/panel/setting"
|
||||
basePath = "/panel/api/setting"
|
||||
case "xray_setting.go":
|
||||
basePath = "/panel/xray"
|
||||
basePath = "/panel/api/xray"
|
||||
case "custom_geo.go":
|
||||
basePath = "/panel/api/custom-geo"
|
||||
case "websocket.go":
|
||||
|
||||
@@ -10,12 +10,9 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// XUIController is the main controller for the X-UI panel, managing sub-controllers.
|
||||
// XUIController is the main controller for the X-UI panel, serving the SPA shell.
|
||||
type XUIController struct {
|
||||
BaseController
|
||||
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
}
|
||||
|
||||
// NewXUIController creates a new XUIController and initializes its routes.
|
||||
@@ -49,9 +46,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
// so they fetch the session token via this endpoint at startup and replay it
|
||||
// on subsequent unsafe requests through axios.
|
||||
g.GET("/csrf-token", a.csrfToken)
|
||||
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
// panelSPA serves the React SPA shell. Every GET under /panel/ that isn't an
|
||||
|
||||
Reference in New Issue
Block a user