| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- import { useEffect, useMemo, useState } from 'react';
- import { useTranslation } from 'react-i18next';
- import {
- Collapse,
- Input,
- InputNumber,
- Select,
- Space,
- Switch,
- } from 'antd';
- import type { AllSetting } from '@/models/setting';
- import { HttpUtil, LanguageManager } from '@/utils';
- import SettingListItem from '@/components/SettingListItem';
- interface ApiMsg<T = unknown> {
- success?: boolean;
- obj?: T;
- }
- interface GeneralTabProps {
- allSetting: AllSetting;
- updateSetting: (patch: Partial<AllSetting>) => void;
- }
- const REMARK_MODELS: Record<string, string> = { i: 'Inbound', e: 'Email', o: 'Other' };
- const REMARK_SEPARATORS = [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'];
- 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 [lang, setLang] = useState<string>(() => LanguageManager.getLanguage());
- const [inboundOptions, setInboundOptions] = 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; };
- }, []);
- const remarkModel = useMemo(() => {
- const rm = allSetting.remarkModel || '';
- return rm.length > 1 ? rm.substring(1).split('') : [];
- }, [allSetting.remarkModel]);
- const remarkSeparator = useMemo(() => {
- const rm = allSetting.remarkModel || '-';
- return rm.length > 1 ? rm.charAt(0) : '-';
- }, [allSetting.remarkModel]);
- const remarkSample = useMemo(() => {
- const parts = remarkModel.map((k) => REMARK_MODELS[k]);
- return parts.length === 0 ? '' : parts.join(remarkSeparator);
- }, [remarkModel, remarkSeparator]);
- function setRemarkModel(parts: string[]) {
- updateSetting({ remarkModel: remarkSeparator + parts.join('') });
- }
- function setRemarkSeparator(sep: string) {
- const tail = (allSetting.remarkModel || '-').substring(1);
- updateSetting({ remarkModel: sep + tail });
- }
- 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 (
- <Collapse defaultActiveKey="1" items={[
- {
- key: '1',
- label: t('pages.settings.panelSettings'),
- children: (
- <>
- <SettingListItem
- paddings="small"
- title={t('pages.settings.remarkModel')}
- description={<>{t('pages.settings.sampleRemark')}: <i>#{remarkSample}</i></>}
- >
- <Space.Compact style={{ width: '100%' }}>
- <Select
- mode="multiple"
- value={remarkModel}
- onChange={setRemarkModel}
- style={{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }}
- options={Object.entries(REMARK_MODELS).map(([k, l]) => ({ value: k, label: l }))}
- />
- <Select
- value={remarkSeparator}
- onChange={setRemarkSeparator}
- style={{ width: '20%' }}
- options={REMARK_SEPARATORS.map((s) => ({ value: s, label: s }))}
- />
- </Space.Compact>
- </SettingListItem>
- <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: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title={t('pages.settings.sessionMaxAge')} description={t('pages.settings.sessionMaxAgeDesc')}>
- <InputNumber value={allSetting.sessionMaxAge} min={60} style={{ width: '100%' }}
- onChange={(v) => updateSetting({ sessionMaxAge: Number(v) || 0 })} />
- </SettingListItem>
- <SettingListItem
- paddings="small"
- title="Trusted proxy CIDRs"
- description="Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers."
- >
- <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.panelProxy')} description={t('pages.settings.panelProxyDesc')}>
- <Input
- value={allSetting.panelProxy}
- placeholder="socks5:// or http://user:pass@host:port"
- onChange={(e) => updateSetting({ panelProxy: e.target.value })}
- />
- </SettingListItem>
- <SettingListItem paddings="small" title={t('pages.settings.pageSize')} description={t('pages.settings.pageSizeDesc')}>
- <InputNumber value={allSetting.pageSize} min={0} 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: t('pages.settings.notifications'),
- 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} style={{ width: '100%' }}
- onChange={(v) => updateSetting({ trafficDiff: Number(v) || 0 })} />
- </SettingListItem>
- </>
- ),
- },
- {
- key: '3',
- label: t('pages.settings.certs'),
- 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: t('pages.settings.externalTraffic'),
- 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: t('pages.settings.dateAndTime'),
- 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: 'LDAP',
- children: (
- <>
- <SettingListItem paddings="small" title="Enable LDAP sync">
- <Switch checked={allSetting.ldapEnable} onChange={(v) => updateSetting({ ldapEnable: v })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="LDAP host">
- <Input value={allSetting.ldapHost} onChange={(e) => updateSetting({ ldapHost: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="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="Use TLS (LDAPS)">
- <Switch checked={allSetting.ldapUseTLS} onChange={(v) => updateSetting({ ldapUseTLS: v })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Bind DN">
- <Input value={allSetting.ldapBindDN} onChange={(e) => updateSetting({ ldapBindDN: e.target.value })} />
- </SettingListItem>
- <SettingListItem
- paddings="small"
- title={t('password')}
- description={allSetting.hasLdapPassword ? 'Configured; leave blank to keep current password.' : 'Not configured.'}
- >
- <Input.Password
- value={allSetting.ldapPassword}
- placeholder={allSetting.hasLdapPassword ? 'Configured - enter a new value to replace' : ''}
- onChange={(e) => updateSetting({ ldapPassword: e.target.value })}
- />
- </SettingListItem>
- <SettingListItem paddings="small" title="Base DN">
- <Input value={allSetting.ldapBaseDN} onChange={(e) => updateSetting({ ldapBaseDN: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="User filter">
- <Input value={allSetting.ldapUserFilter} onChange={(e) => updateSetting({ ldapUserFilter: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="User attribute (username/email)">
- <Input value={allSetting.ldapUserAttr} onChange={(e) => updateSetting({ ldapUserAttr: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="VLESS flag attribute">
- <Input value={allSetting.ldapVlessField} onChange={(e) => updateSetting({ ldapVlessField: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Generic flag attribute (optional)" description="If set, overrides VLESS flag — e.g. shadowInactive.">
- <Input value={allSetting.ldapFlagField} onChange={(e) => updateSetting({ ldapFlagField: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Truthy values" description="Comma-separated; default: true,1,yes,on">
- <Input value={allSetting.ldapTruthyValues} onChange={(e) => updateSetting({ ldapTruthyValues: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Invert flag" description="Enable when the attribute means disabled (e.g. shadowInactive).">
- <Switch checked={allSetting.ldapInvertFlag} onChange={(v) => updateSetting({ ldapInvertFlag: v })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Sync schedule" description="Cron-like string, e.g. @every 1m">
- <Input value={allSetting.ldapSyncCron} onChange={(e) => updateSetting({ ldapSyncCron: e.target.value })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Inbound tags" description="Inbounds that LDAP sync may auto-create or auto-delete clients on.">
- <>
- <Select
- mode="multiple"
- value={ldapInboundTagList}
- onChange={setLdapInboundTagList}
- style={{ width: '100%' }}
- options={inboundOptions}
- />
- {inboundOptions.length === 0 && (
- <div className="ldap-no-inbounds">No inbounds found. Create one in Inbounds first.</div>
- )}
- </>
- </SettingListItem>
- <SettingListItem paddings="small" title="Auto create clients">
- <Switch checked={allSetting.ldapAutoCreate} onChange={(v) => updateSetting({ ldapAutoCreate: v })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Auto delete clients">
- <Switch checked={allSetting.ldapAutoDelete} onChange={(v) => updateSetting({ ldapAutoDelete: v })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Default total (GB)">
- <InputNumber value={allSetting.ldapDefaultTotalGB} min={0} style={{ width: '100%' }}
- onChange={(v) => updateSetting({ ldapDefaultTotalGB: Number(v) || 0 })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Default expiry (days)">
- <InputNumber value={allSetting.ldapDefaultExpiryDays} min={0} style={{ width: '100%' }}
- onChange={(v) => updateSetting({ ldapDefaultExpiryDays: Number(v) || 0 })} />
- </SettingListItem>
- <SettingListItem paddings="small" title="Default IP limit">
- <InputNumber value={allSetting.ldapDefaultLimitIP} min={0} style={{ width: '100%' }}
- onChange={(v) => updateSetting({ ldapDefaultLimitIP: Number(v) || 0 })} />
- </SettingListItem>
- </>
- ),
- },
- ]} />
- );
- }
|