mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
New /panel/api-docs route with a one-page reference covering every /panel/api/* endpoint (Auth, Inbounds, Server, Nodes, Custom Geo, Backup) plus a Bearer-token primer that reads the current token and exposes Show/Copy/Regenerate inline. Sidebar gets an API Docs entry right after Xray; the menu label is shared via menu.apiDocs across all 13 locales.
196 lines
5.8 KiB
JavaScript
196 lines
5.8 KiB
JavaScript
import { defineConfig } from 'vite';
|
|
import vue from '@vitejs/plugin-vue';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
|
|
const outDir = path.resolve(__dirname, '../web/dist');
|
|
const BACKEND_TARGET = 'http://localhost:2053';
|
|
|
|
function resolveDBPath() {
|
|
const envFolder = process.env.XUI_DB_FOLDER;
|
|
if (envFolder) return path.join(envFolder, 'x-ui.db');
|
|
const repoDB = path.resolve(__dirname, '..', 'x-ui.db');
|
|
if (fs.existsSync(repoDB)) return repoDB;
|
|
return '/etc/x-ui/x-ui.db';
|
|
}
|
|
|
|
const BASE_MIGRATED_ROUTES = {
|
|
'panel': '/index.html',
|
|
'panel/': '/index.html',
|
|
'panel/settings': '/settings.html',
|
|
'panel/settings/': '/settings.html',
|
|
'panel/inbounds': '/inbounds.html',
|
|
'panel/inbounds/': '/inbounds.html',
|
|
'panel/xray': '/xray.html',
|
|
'panel/xray/': '/xray.html',
|
|
'panel/nodes': '/nodes.html',
|
|
'panel/nodes/': '/nodes.html',
|
|
'panel/api-docs': '/api-docs.html',
|
|
'panel/api-docs/': '/api-docs.html',
|
|
};
|
|
|
|
let cachedBasePath = '/';
|
|
|
|
function readBasePathFromDB() {
|
|
const dbPath = resolveDBPath();
|
|
let db;
|
|
try {
|
|
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
} catch (_e) {
|
|
return '/';
|
|
}
|
|
try {
|
|
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get('webBasePath');
|
|
let value = row && typeof row.value === 'string' ? row.value : '/';
|
|
if (!value.startsWith('/')) value = '/' + value;
|
|
if (!value.endsWith('/')) value += '/';
|
|
return value;
|
|
} catch (_e) {
|
|
return '/';
|
|
} finally {
|
|
db.close();
|
|
}
|
|
}
|
|
|
|
function refreshBasePath() {
|
|
cachedBasePath = readBasePathFromDB();
|
|
return cachedBasePath;
|
|
}
|
|
|
|
// `apply: 'serve'` keeps the injection out of `vite build` — dist.go
|
|
// already injects webBasePath at runtime in production.
|
|
function injectBasePathPlugin() {
|
|
return {
|
|
name: 'xui-inject-base-path',
|
|
apply: 'serve',
|
|
transformIndexHtml(html) {
|
|
const basePath = refreshBasePath();
|
|
const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
const tag = `<script>window.X_UI_BASE_PATH="${escaped}";</script>`;
|
|
return html.replace('</head>', `${tag}</head>`);
|
|
},
|
|
};
|
|
}
|
|
|
|
function bypassMigratedRoute(req) {
|
|
if (req.method !== 'GET') return undefined;
|
|
const url = req.url.split('?')[0];
|
|
|
|
for (const [key, value] of Object.entries(BASE_MIGRATED_ROUTES)) {
|
|
if (url === '/' + key) return value;
|
|
}
|
|
|
|
const m = url.match(/^\/[^/]+\/(.+)$/);
|
|
if (m) {
|
|
const stripped = m[1];
|
|
if (stripped in BASE_MIGRATED_ROUTES) return BASE_MIGRATED_ROUTES[stripped];
|
|
}
|
|
|
|
if (url === '/' || /^\/[^/]+\/$/.test(url)) return '/login.html';
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function rewriteToBackend(p) {
|
|
if (cachedBasePath === '/' || p.startsWith(cachedBasePath)) return p;
|
|
return cachedBasePath + p.replace(/^\//, '');
|
|
}
|
|
|
|
function makeBackendProxy(target) {
|
|
return {
|
|
target,
|
|
changeOrigin: true,
|
|
rewrite: rewriteToBackend,
|
|
bypass: bypassMigratedRoute,
|
|
configure(proxy) {
|
|
let warned = false;
|
|
proxy.on('error', (err, req) => {
|
|
const codes = new Set();
|
|
if (err && err.code) codes.add(err.code);
|
|
if (err && Array.isArray(err.errors)) {
|
|
for (const inner of err.errors) {
|
|
if (inner && inner.code) codes.add(inner.code);
|
|
}
|
|
}
|
|
const offline = codes.has('ECONNREFUSED') || codes.has('ECONNRESET');
|
|
if (offline) {
|
|
if (!warned) {
|
|
warned = true;
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
`[proxy] backend ${target} is not reachable — start the Go server (e.g. \`go run main.go\`) to forward ${req?.url || 'requests'}.`,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
// eslint-disable-next-line no-console
|
|
console.error('[proxy]', err);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [vue(), injectBasePathPlugin()],
|
|
resolve: {
|
|
alias: {
|
|
'@': path.resolve(__dirname, 'src'),
|
|
},
|
|
},
|
|
build: {
|
|
outDir,
|
|
emptyOutDir: true,
|
|
sourcemap: true,
|
|
target: 'es2020',
|
|
chunkSizeWarningLimit: 1500,
|
|
rollupOptions: {
|
|
input: {
|
|
index: path.resolve(__dirname, 'index.html'),
|
|
login: path.resolve(__dirname, 'login.html'),
|
|
settings: path.resolve(__dirname, 'settings.html'),
|
|
inbounds: path.resolve(__dirname, 'inbounds.html'),
|
|
xray: path.resolve(__dirname, 'xray.html'),
|
|
nodes: path.resolve(__dirname, 'nodes.html'),
|
|
apiDocs: path.resolve(__dirname, 'api-docs.html'),
|
|
subpage: path.resolve(__dirname, 'subpage.html'),
|
|
},
|
|
output: {
|
|
manualChunks(id) {
|
|
if (!id.includes('node_modules')) return undefined;
|
|
if (id.includes('ant-design-vue')) return 'vendor-antd';
|
|
if (id.includes('@ant-design/icons-vue')) return 'vendor-icons';
|
|
if (id.includes('vue-i18n')) return 'vendor-i18n';
|
|
if (
|
|
id.includes('/node_modules/vue/')
|
|
|| id.includes('/node_modules/@vue/')
|
|
) return 'vendor-vue';
|
|
if (id.includes('dayjs')) return 'vendor-dayjs';
|
|
if (id.includes('axios')) return 'vendor-axios';
|
|
if (
|
|
id.includes('vue3-persian-datetime-picker')
|
|
|| id.includes('moment-jalaali')
|
|
|| id.includes('jalaali-js')
|
|
|| id.includes('/node_modules/moment/')
|
|
) return 'vendor-jalali';
|
|
return 'vendor';
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
port: 5173,
|
|
strictPort: true,
|
|
proxy: {
|
|
'^/(?:[^/]+/)?(login|logout|getTwoFactorEnable|csrf-token|panel|server)(?:/|$)': makeBackendProxy(BACKEND_TARGET),
|
|
'^/$': makeBackendProxy(BACKEND_TARGET),
|
|
'^/[^/]+/$': makeBackendProxy(BACKEND_TARGET),
|
|
'^/(?:[^/]+/)?ws$': {
|
|
target: 'ws://localhost:2053',
|
|
ws: true,
|
|
changeOrigin: true,
|
|
rewrite: rewriteToBackend,
|
|
},
|
|
},
|
|
},
|
|
});
|