Просмотр исходного кода

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.
MHSanaei 1 день назад
Родитель
Сommit
c6f15cd53f

Разница между файлами не показана из-за своего большого размера
+ 1785 - 1
frontend/public/openapi.json


+ 2 - 2
frontend/src/api/queries/useAllSettings.ts

@@ -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() });

+ 1 - 1
frontend/src/hooks/useClients.ts

@@ -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 || {};

+ 1 - 1
frontend/src/hooks/useDatepicker.ts

@@ -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';

+ 7 - 7
frontend/src/hooks/useXraySetting.ts

@@ -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,

+ 23 - 23
frontend/src/pages/api-docs/endpoints.ts

@@ -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 &lt;token&gt;</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 &lt;token&gt;</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).' },

+ 1 - 1
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -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;

+ 1 - 1
frontend/src/pages/inbounds/useInbounds.ts

@@ -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 ?? {};

+ 1 - 1
frontend/src/pages/index/BackupModal.tsx

@@ -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();

+ 1 - 1
frontend/src/pages/index/IndexPage.tsx

@@ -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) => {

+ 5 - 5
frontend/src/pages/settings/SecurityTab.tsx

@@ -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)));
     }

+ 1 - 1
frontend/src/pages/settings/SettingsPage.tsx

@@ -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());

+ 6 - 6
frontend/src/pages/xray/overrides/NordModal.tsx

@@ -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 || [];

+ 5 - 5
frontend/src/pages/xray/overrides/WarpModal.tsx

@@ -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);

+ 1 - 1
frontend/vite.config.js

@@ -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 = '/';
 

+ 15 - 7
web/controller/api.go

@@ -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)
 }

+ 2 - 2
web/controller/api_docs_test.go

@@ -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":

+ 1 - 7
web/controller/xui.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

Некоторые файлы не были показаны из-за большого количества измененных файлов