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:
MHSanaei
2026-06-06 16:22:41 +02:00
parent a014c01725
commit c6f15cd53f
18 changed files with 1928 additions and 121 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -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).' },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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