mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
ca4f32e3da
Instead of requiring a manual SOCKS5/HTTP URL, the panel now lets the admin pick an Xray outbound from a dropdown (same UX as Geodata Auto-Update). At runtime, injectPanelEgress appends a loopback SOCKS inbound (tag: panel-egress) and prepends a routing rule so the panel's own HTTP traffic — version checks, Telegram, normal geo-file updates — is routed through the chosen outbound. Xray-native Geodata Auto-Update is unaffected (it uses its own geodata.outbound inside Xray). Blackhole outbounds are excluded from both picker dropdowns since routing any download through one just drops it. Translations updated for all 13 locales.
363 lines
18 KiB
TypeScript
363 lines
18 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Input,
|
|
InputNumber,
|
|
Select,
|
|
Switch,
|
|
Tabs,
|
|
} from 'antd';
|
|
import {
|
|
ApartmentOutlined,
|
|
BellOutlined,
|
|
ClockCircleOutlined,
|
|
GlobalOutlined,
|
|
SafetyCertificateOutlined,
|
|
SettingOutlined,
|
|
} from '@ant-design/icons';
|
|
import type { AllSetting } from '@/models/setting';
|
|
import { HttpUtil, LanguageManager } from '@/utils';
|
|
import { SettingListItem } from '@/components/ui';
|
|
import { useMediaQuery } from '@/hooks/useMediaQuery';
|
|
import { catTabLabel } from './catTabLabel';
|
|
import { sanitizePath } from './uriPath';
|
|
|
|
interface ApiMsg<T = unknown> {
|
|
success?: boolean;
|
|
obj?: T;
|
|
}
|
|
|
|
interface GeneralTabProps {
|
|
allSetting: AllSetting;
|
|
updateSetting: (patch: Partial<AllSetting>) => void;
|
|
}
|
|
|
|
const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [
|
|
{ name: 'Gregorian (Standard)', value: 'gregorian' },
|
|
{ name: 'Jalalian (شمسی)', value: 'jalalian' },
|
|
];
|
|
|
|
export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProps) {
|
|
const { t } = useTranslation();
|
|
const { isMobile } = useMediaQuery();
|
|
|
|
const [lang, setLang] = useState<string>(() => LanguageManager.getLanguage());
|
|
const [inboundOptions, setInboundOptions] = useState<{ label: string; value: string }[]>([]);
|
|
const [outboundOptions, setOutboundOptions] = useState<{ label: string; value: string }[]>([]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
// /options is the slim picker-shaped endpoint — it skips the heavy
|
|
// per-client settings and clientStats payloads that /list ships.
|
|
const msg = await HttpUtil.get('/panel/api/inbounds/options') as ApiMsg<{
|
|
tag: string; protocol: string; port: number;
|
|
}[]>;
|
|
if (cancelled) return;
|
|
if (msg?.success && Array.isArray(msg.obj)) {
|
|
setInboundOptions(msg.obj.map((ib) => ({
|
|
label: `${ib.tag} (${ib.protocol}@${ib.port})`,
|
|
value: ib.tag,
|
|
})));
|
|
} else {
|
|
setInboundOptions([]);
|
|
}
|
|
})();
|
|
return () => { cancelled = true; };
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
// Outbound tags for the panel egress picker: template outbounds plus
|
|
// subscription-derived outbounds, same candidate set as the geodata
|
|
// download picker.
|
|
const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true }) as ApiMsg<string>;
|
|
if (cancelled || !msg?.success || typeof msg.obj !== 'string') return;
|
|
try {
|
|
const payload = JSON.parse(msg.obj) as Record<string, unknown>;
|
|
const template = (payload.xraySetting || {}) as Record<string, unknown>;
|
|
const tags = new Set<string>();
|
|
const outbounds = Array.isArray(template.outbounds) ? template.outbounds : [];
|
|
for (const o of outbounds) {
|
|
if (!o || typeof o !== 'object') continue;
|
|
const rec = o as Record<string, unknown>;
|
|
if (rec.protocol === 'blackhole') continue; // dropping traffic is never a useful egress
|
|
const tag = rec.tag;
|
|
if (typeof tag === 'string' && tag) tags.add(tag);
|
|
}
|
|
const subTags = Array.isArray(payload.subscriptionOutboundTags) ? payload.subscriptionOutboundTags : [];
|
|
for (const tag of subTags) {
|
|
if (typeof tag === 'string' && tag) tags.add(tag);
|
|
}
|
|
setOutboundOptions([...tags].map((tag) => ({ label: tag, value: tag })));
|
|
} catch {
|
|
setOutboundOptions([]);
|
|
}
|
|
})();
|
|
return () => { cancelled = true; };
|
|
}, []);
|
|
|
|
const ldapInboundTagList = useMemo(() => {
|
|
const csv = allSetting.ldapInboundTags || '';
|
|
return csv.length ? csv.split(',').map((s) => s.trim()).filter(Boolean) : [];
|
|
}, [allSetting.ldapInboundTags]);
|
|
|
|
function setLdapInboundTagList(list: string[]) {
|
|
updateSetting({ ldapInboundTags: Array.isArray(list) ? list.join(',') : '' });
|
|
}
|
|
|
|
function onLangChange(value: string) {
|
|
setLang(value);
|
|
LanguageManager.setLanguage(value);
|
|
}
|
|
|
|
const langOptions = useMemo(
|
|
() => LanguageManager.supportedLanguages.map((l: { value: string; name: string; icon: string }) => ({
|
|
value: l.value,
|
|
label: (
|
|
<>
|
|
<span role="img" aria-label={l.name}>{l.icon}</span>
|
|
<span>{l.name}</span>
|
|
</>
|
|
),
|
|
})),
|
|
[],
|
|
);
|
|
|
|
return (
|
|
<Tabs defaultActiveKey="1" items={[
|
|
{
|
|
key: '1',
|
|
label: catTabLabel(<SettingOutlined />, t('pages.settings.panelSettings'), isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.panelListeningIP')} description={t('pages.settings.panelListeningIPDesc')}>
|
|
<Input value={allSetting.webListen} onChange={(e) => updateSetting({ webListen: e.target.value })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.panelListeningDomain')} description={t('pages.settings.panelListeningDomainDesc')}>
|
|
<Input value={allSetting.webDomain} onChange={(e) => updateSetting({ webDomain: e.target.value })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.panelPort')} description={t('pages.settings.panelPortDesc')}>
|
|
<InputNumber value={allSetting.webPort} min={1} max={65535} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ webPort: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.panelUrlPath')} description={t('pages.settings.panelUrlPathDesc')}>
|
|
<Input value={allSetting.webBasePath} onChange={(e) => updateSetting({ webBasePath: sanitizePath(e.target.value) })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.sessionMaxAge')} description={t('pages.settings.sessionMaxAgeDesc')}>
|
|
<InputNumber value={allSetting.sessionMaxAge} min={60} max={525600} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ sessionMaxAge: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem
|
|
paddings="small"
|
|
title={t('pages.settings.trustedProxyCidrs')}
|
|
description={t('pages.settings.trustedProxyCidrsDesc')}
|
|
>
|
|
<Input
|
|
value={allSetting.trustedProxyCIDRs}
|
|
placeholder="127.0.0.1/32,::1/128"
|
|
onChange={(e) => updateSetting({ trustedProxyCIDRs: e.target.value })}
|
|
/>
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.panelOutbound')} description={t('pages.settings.panelOutboundDesc')}>
|
|
<Select
|
|
style={{ width: '100%' }}
|
|
allowClear
|
|
showSearch
|
|
value={allSetting.panelOutbound || undefined}
|
|
placeholder={t('pages.settings.panelOutboundPh')}
|
|
options={outboundOptions}
|
|
onChange={(v) => updateSetting({ panelOutbound: (v as string | undefined) || '' })}
|
|
/>
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.pageSize')} description={t('pages.settings.pageSizeDesc')}>
|
|
<InputNumber value={allSetting.pageSize} min={0} max={1000} step={5} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ pageSize: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
|
|
<SettingListItem paddings="small" title={t('pages.settings.language')}>
|
|
<Select
|
|
value={lang}
|
|
onChange={onLangChange}
|
|
style={{ width: '100%' }}
|
|
options={langOptions}
|
|
/>
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '2',
|
|
label: catTabLabel(<BellOutlined />, t('pages.settings.notifications'), isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.expireTimeDiff')} description={t('pages.settings.expireTimeDiffDesc')}>
|
|
<InputNumber value={allSetting.expireDiff} min={0} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ expireDiff: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.trafficDiff')} description={t('pages.settings.trafficDiffDesc')}>
|
|
<InputNumber value={allSetting.trafficDiff} min={0} max={100} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ trafficDiff: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '3',
|
|
label: catTabLabel(<SafetyCertificateOutlined />, t('pages.settings.certs'), isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.publicKeyPath')} description={t('pages.settings.publicKeyPathDesc')}>
|
|
<Input value={allSetting.webCertFile} onChange={(e) => updateSetting({ webCertFile: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.privateKeyPath')} description={t('pages.settings.privateKeyPathDesc')}>
|
|
<Input value={allSetting.webKeyFile} onChange={(e) => updateSetting({ webKeyFile: e.target.value })} />
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '4',
|
|
label: catTabLabel(<GlobalOutlined />, t('pages.settings.externalTraffic'), isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.externalTrafficInformEnable')} description={t('pages.settings.externalTrafficInformEnableDesc')}>
|
|
<Switch checked={allSetting.externalTrafficInformEnable}
|
|
onChange={(v) => updateSetting({ externalTrafficInformEnable: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.externalTrafficInformURI')} description={t('pages.settings.externalTrafficInformURIDesc')}>
|
|
<Input
|
|
value={allSetting.externalTrafficInformURI}
|
|
placeholder="(http|https)://domain[:port]/path/"
|
|
onChange={(e) => updateSetting({ externalTrafficInformURI: e.target.value })}
|
|
/>
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.restartXrayOnClientDisable')} description={t('pages.settings.restartXrayOnClientDisableDesc')}>
|
|
<Switch checked={allSetting.restartXrayOnClientDisable}
|
|
onChange={(v) => updateSetting({ restartXrayOnClientDisable: v })} />
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '5',
|
|
label: catTabLabel(<ClockCircleOutlined />, t('pages.settings.dateAndTime'), isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.timeZone')} description={t('pages.settings.timeZoneDesc')}>
|
|
<Input value={allSetting.timeLocation} onChange={(e) => updateSetting({ timeLocation: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.datepicker')} description={t('pages.settings.datepickerDescription')}>
|
|
<Select
|
|
value={allSetting.datepicker || 'gregorian'}
|
|
onChange={(v) => updateSetting({ datepicker: v as 'gregorian' | 'jalalian' })}
|
|
style={{ width: '100%' }}
|
|
options={DATEPICKER_LIST.map((d) => ({ value: d.value, label: d.name }))}
|
|
/>
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
{
|
|
key: '6',
|
|
label: catTabLabel(<ApartmentOutlined />, 'LDAP', isMobile),
|
|
children: (
|
|
<>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.enable')}>
|
|
<Switch checked={allSetting.ldapEnable} onChange={(v) => updateSetting({ ldapEnable: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.host')}>
|
|
<Input value={allSetting.ldapHost} onChange={(e) => updateSetting({ ldapHost: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.port')}>
|
|
<InputNumber value={allSetting.ldapPort} min={1} max={65535} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ ldapPort: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.useTls')}>
|
|
<Switch checked={allSetting.ldapUseTLS} onChange={(v) => updateSetting({ ldapUseTLS: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.bindDn')}>
|
|
<Input value={allSetting.ldapBindDN} onChange={(e) => updateSetting({ ldapBindDN: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem
|
|
paddings="small"
|
|
title={t('password')}
|
|
description={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordConfigured') : t('pages.settings.ldap.passwordUnconfigured')}
|
|
>
|
|
<Input.Password
|
|
value={allSetting.ldapPassword}
|
|
placeholder={allSetting.hasLdapPassword ? t('pages.settings.ldap.passwordPlaceholder') : ''}
|
|
onChange={(e) => updateSetting({ ldapPassword: e.target.value })}
|
|
/>
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.baseDn')}>
|
|
<Input value={allSetting.ldapBaseDN} onChange={(e) => updateSetting({ ldapBaseDN: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.userFilter')}>
|
|
<Input value={allSetting.ldapUserFilter} onChange={(e) => updateSetting({ ldapUserFilter: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.userAttr')}>
|
|
<Input value={allSetting.ldapUserAttr} onChange={(e) => updateSetting({ ldapUserAttr: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.vlessField')}>
|
|
<Input value={allSetting.ldapVlessField} onChange={(e) => updateSetting({ ldapVlessField: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.flagField')} description={t('pages.settings.ldap.flagFieldDesc')}>
|
|
<Input value={allSetting.ldapFlagField} onChange={(e) => updateSetting({ ldapFlagField: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.truthyValues')} description={t('pages.settings.ldap.truthyValuesDesc')}>
|
|
<Input value={allSetting.ldapTruthyValues} onChange={(e) => updateSetting({ ldapTruthyValues: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.invertFlag')} description={t('pages.settings.ldap.invertFlagDesc')}>
|
|
<Switch checked={allSetting.ldapInvertFlag} onChange={(v) => updateSetting({ ldapInvertFlag: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.syncSchedule')} description={t('pages.settings.ldap.syncScheduleDesc')}>
|
|
<Input value={allSetting.ldapSyncCron} onChange={(e) => updateSetting({ ldapSyncCron: e.target.value })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.inboundTags')} description={t('pages.settings.ldap.inboundTagsDesc')}>
|
|
<>
|
|
<Select
|
|
mode="multiple"
|
|
value={ldapInboundTagList}
|
|
onChange={setLdapInboundTagList}
|
|
style={{ width: '100%' }}
|
|
options={inboundOptions}
|
|
/>
|
|
{inboundOptions.length === 0 && (
|
|
<div className="ldap-no-inbounds">{t('pages.settings.ldap.noInbounds')}</div>
|
|
)}
|
|
</>
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoCreate')}>
|
|
<Switch checked={allSetting.ldapAutoCreate} onChange={(v) => updateSetting({ ldapAutoCreate: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.autoDelete')}>
|
|
<Switch checked={allSetting.ldapAutoDelete} onChange={(v) => updateSetting({ ldapAutoDelete: v })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultTotalGb')}>
|
|
<InputNumber value={allSetting.ldapDefaultTotalGB} min={0} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ ldapDefaultTotalGB: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultExpiryDays')}>
|
|
<InputNumber value={allSetting.ldapDefaultExpiryDays} min={0} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ ldapDefaultExpiryDays: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
<SettingListItem paddings="small" title={t('pages.settings.ldap.defaultIpLimit')}>
|
|
<InputNumber value={allSetting.ldapDefaultLimitIP} min={0} style={{ width: '100%' }}
|
|
onChange={(v) => updateSetting({ ldapDefaultLimitIP: Number(v) || 0 })} />
|
|
</SettingListItem>
|
|
</>
|
|
),
|
|
},
|
|
]} />
|
|
);
|
|
}
|