GeneralTab.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Collapse,
  5. Input,
  6. InputNumber,
  7. Select,
  8. Space,
  9. Switch,
  10. } from 'antd';
  11. import type { AllSetting } from '@/models/setting';
  12. import { HttpUtil, LanguageManager } from '@/utils';
  13. import SettingListItem from '@/components/SettingListItem';
  14. interface ApiMsg<T = unknown> {
  15. success?: boolean;
  16. obj?: T;
  17. }
  18. interface GeneralTabProps {
  19. allSetting: AllSetting;
  20. updateSetting: (patch: Partial<AllSetting>) => void;
  21. }
  22. const REMARK_MODELS: Record<string, string> = { i: 'Inbound', e: 'Email', o: 'Other' };
  23. const REMARK_SEPARATORS = [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'];
  24. const DATEPICKER_LIST: { name: string; value: 'gregorian' | 'jalalian' }[] = [
  25. { name: 'Gregorian (Standard)', value: 'gregorian' },
  26. { name: 'Jalalian (شمسی)', value: 'jalalian' },
  27. ];
  28. export default function GeneralTab({ allSetting, updateSetting }: GeneralTabProps) {
  29. const { t } = useTranslation();
  30. const [lang, setLang] = useState<string>(() => LanguageManager.getLanguage());
  31. const [inboundOptions, setInboundOptions] = useState<{ label: string; value: string }[]>([]);
  32. useEffect(() => {
  33. let cancelled = false;
  34. (async () => {
  35. // /options is the slim picker-shaped endpoint — it skips the heavy
  36. // per-client settings and clientStats payloads that /list ships.
  37. const msg = await HttpUtil.get('/panel/api/inbounds/options') as ApiMsg<{
  38. tag: string; protocol: string; port: number;
  39. }[]>;
  40. if (cancelled) return;
  41. if (msg?.success && Array.isArray(msg.obj)) {
  42. setInboundOptions(msg.obj.map((ib) => ({
  43. label: `${ib.tag} (${ib.protocol}@${ib.port})`,
  44. value: ib.tag,
  45. })));
  46. } else {
  47. setInboundOptions([]);
  48. }
  49. })();
  50. return () => { cancelled = true; };
  51. }, []);
  52. const remarkModel = useMemo(() => {
  53. const rm = allSetting.remarkModel || '';
  54. return rm.length > 1 ? rm.substring(1).split('') : [];
  55. }, [allSetting.remarkModel]);
  56. const remarkSeparator = useMemo(() => {
  57. const rm = allSetting.remarkModel || '-';
  58. return rm.length > 1 ? rm.charAt(0) : '-';
  59. }, [allSetting.remarkModel]);
  60. const remarkSample = useMemo(() => {
  61. const parts = remarkModel.map((k) => REMARK_MODELS[k]);
  62. return parts.length === 0 ? '' : parts.join(remarkSeparator);
  63. }, [remarkModel, remarkSeparator]);
  64. function setRemarkModel(parts: string[]) {
  65. updateSetting({ remarkModel: remarkSeparator + parts.join('') });
  66. }
  67. function setRemarkSeparator(sep: string) {
  68. const tail = (allSetting.remarkModel || '-').substring(1);
  69. updateSetting({ remarkModel: sep + tail });
  70. }
  71. const ldapInboundTagList = useMemo(() => {
  72. const csv = allSetting.ldapInboundTags || '';
  73. return csv.length ? csv.split(',').map((s) => s.trim()).filter(Boolean) : [];
  74. }, [allSetting.ldapInboundTags]);
  75. function setLdapInboundTagList(list: string[]) {
  76. updateSetting({ ldapInboundTags: Array.isArray(list) ? list.join(',') : '' });
  77. }
  78. function onLangChange(value: string) {
  79. setLang(value);
  80. LanguageManager.setLanguage(value);
  81. }
  82. const langOptions = useMemo(
  83. () => LanguageManager.supportedLanguages.map((l: { value: string; name: string; icon: string }) => ({
  84. value: l.value,
  85. label: (
  86. <>
  87. <span role="img" aria-label={l.name}>{l.icon}</span>
  88. &nbsp;&nbsp;<span>{l.name}</span>
  89. </>
  90. ),
  91. })),
  92. [],
  93. );
  94. return (
  95. <Collapse defaultActiveKey="1" items={[
  96. {
  97. key: '1',
  98. label: t('pages.settings.panelSettings'),
  99. children: (
  100. <>
  101. <SettingListItem
  102. paddings="small"
  103. title={t('pages.settings.remarkModel')}
  104. description={<>{t('pages.settings.sampleRemark')}: <i>#{remarkSample}</i></>}
  105. >
  106. <Space.Compact style={{ width: '100%' }}>
  107. <Select
  108. mode="multiple"
  109. value={remarkModel}
  110. onChange={setRemarkModel}
  111. style={{ paddingRight: '.5rem', minWidth: '80%', width: 'auto' }}
  112. options={Object.entries(REMARK_MODELS).map(([k, l]) => ({ value: k, label: l }))}
  113. />
  114. <Select
  115. value={remarkSeparator}
  116. onChange={setRemarkSeparator}
  117. style={{ width: '20%' }}
  118. options={REMARK_SEPARATORS.map((s) => ({ value: s, label: s }))}
  119. />
  120. </Space.Compact>
  121. </SettingListItem>
  122. <SettingListItem paddings="small" title={t('pages.settings.panelListeningIP')} description={t('pages.settings.panelListeningIPDesc')}>
  123. <Input value={allSetting.webListen} onChange={(e) => updateSetting({ webListen: e.target.value })} />
  124. </SettingListItem>
  125. <SettingListItem paddings="small" title={t('pages.settings.panelListeningDomain')} description={t('pages.settings.panelListeningDomainDesc')}>
  126. <Input value={allSetting.webDomain} onChange={(e) => updateSetting({ webDomain: e.target.value })} />
  127. </SettingListItem>
  128. <SettingListItem paddings="small" title={t('pages.settings.panelPort')} description={t('pages.settings.panelPortDesc')}>
  129. <InputNumber value={allSetting.webPort} min={1} max={65535} style={{ width: '100%' }}
  130. onChange={(v) => updateSetting({ webPort: Number(v) || 0 })} />
  131. </SettingListItem>
  132. <SettingListItem paddings="small" title={t('pages.settings.panelUrlPath')} description={t('pages.settings.panelUrlPathDesc')}>
  133. <Input value={allSetting.webBasePath} onChange={(e) => updateSetting({ webBasePath: e.target.value })} />
  134. </SettingListItem>
  135. <SettingListItem paddings="small" title={t('pages.settings.sessionMaxAge')} description={t('pages.settings.sessionMaxAgeDesc')}>
  136. <InputNumber value={allSetting.sessionMaxAge} min={60} style={{ width: '100%' }}
  137. onChange={(v) => updateSetting({ sessionMaxAge: Number(v) || 0 })} />
  138. </SettingListItem>
  139. <SettingListItem
  140. paddings="small"
  141. title="Trusted proxy CIDRs"
  142. description="Comma-separated IPs/CIDRs allowed to set forwarded host, proto, and client IP headers."
  143. >
  144. <Input
  145. value={allSetting.trustedProxyCIDRs}
  146. placeholder="127.0.0.1/32,::1/128"
  147. onChange={(e) => updateSetting({ trustedProxyCIDRs: e.target.value })}
  148. />
  149. </SettingListItem>
  150. <SettingListItem paddings="small" title={t('pages.settings.panelProxy')} description={t('pages.settings.panelProxyDesc')}>
  151. <Input
  152. value={allSetting.panelProxy}
  153. placeholder="socks5:// or http://user:pass@host:port"
  154. onChange={(e) => updateSetting({ panelProxy: e.target.value })}
  155. />
  156. </SettingListItem>
  157. <SettingListItem paddings="small" title={t('pages.settings.pageSize')} description={t('pages.settings.pageSizeDesc')}>
  158. <InputNumber value={allSetting.pageSize} min={0} step={5} style={{ width: '100%' }}
  159. onChange={(v) => updateSetting({ pageSize: Number(v) || 0 })} />
  160. </SettingListItem>
  161. <SettingListItem paddings="small" title={t('pages.settings.language')}>
  162. <Select
  163. value={lang}
  164. onChange={onLangChange}
  165. style={{ width: '100%' }}
  166. options={langOptions}
  167. />
  168. </SettingListItem>
  169. </>
  170. ),
  171. },
  172. {
  173. key: '2',
  174. label: t('pages.settings.notifications'),
  175. children: (
  176. <>
  177. <SettingListItem paddings="small" title={t('pages.settings.expireTimeDiff')} description={t('pages.settings.expireTimeDiffDesc')}>
  178. <InputNumber value={allSetting.expireDiff} min={0} style={{ width: '100%' }}
  179. onChange={(v) => updateSetting({ expireDiff: Number(v) || 0 })} />
  180. </SettingListItem>
  181. <SettingListItem paddings="small" title={t('pages.settings.trafficDiff')} description={t('pages.settings.trafficDiffDesc')}>
  182. <InputNumber value={allSetting.trafficDiff} min={0} style={{ width: '100%' }}
  183. onChange={(v) => updateSetting({ trafficDiff: Number(v) || 0 })} />
  184. </SettingListItem>
  185. </>
  186. ),
  187. },
  188. {
  189. key: '3',
  190. label: t('pages.settings.certs'),
  191. children: (
  192. <>
  193. <SettingListItem paddings="small" title={t('pages.settings.publicKeyPath')} description={t('pages.settings.publicKeyPathDesc')}>
  194. <Input value={allSetting.webCertFile} onChange={(e) => updateSetting({ webCertFile: e.target.value })} />
  195. </SettingListItem>
  196. <SettingListItem paddings="small" title={t('pages.settings.privateKeyPath')} description={t('pages.settings.privateKeyPathDesc')}>
  197. <Input value={allSetting.webKeyFile} onChange={(e) => updateSetting({ webKeyFile: e.target.value })} />
  198. </SettingListItem>
  199. </>
  200. ),
  201. },
  202. {
  203. key: '4',
  204. label: t('pages.settings.externalTraffic'),
  205. children: (
  206. <>
  207. <SettingListItem paddings="small" title={t('pages.settings.externalTrafficInformEnable')} description={t('pages.settings.externalTrafficInformEnableDesc')}>
  208. <Switch checked={allSetting.externalTrafficInformEnable}
  209. onChange={(v) => updateSetting({ externalTrafficInformEnable: v })} />
  210. </SettingListItem>
  211. <SettingListItem paddings="small" title={t('pages.settings.externalTrafficInformURI')} description={t('pages.settings.externalTrafficInformURIDesc')}>
  212. <Input
  213. value={allSetting.externalTrafficInformURI}
  214. placeholder="(http|https)://domain[:port]/path/"
  215. onChange={(e) => updateSetting({ externalTrafficInformURI: e.target.value })}
  216. />
  217. </SettingListItem>
  218. <SettingListItem paddings="small" title={t('pages.settings.restartXrayOnClientDisable')} description={t('pages.settings.restartXrayOnClientDisableDesc')}>
  219. <Switch checked={allSetting.restartXrayOnClientDisable}
  220. onChange={(v) => updateSetting({ restartXrayOnClientDisable: v })} />
  221. </SettingListItem>
  222. </>
  223. ),
  224. },
  225. {
  226. key: '5',
  227. label: t('pages.settings.dateAndTime'),
  228. children: (
  229. <>
  230. <SettingListItem paddings="small" title={t('pages.settings.timeZone')} description={t('pages.settings.timeZoneDesc')}>
  231. <Input value={allSetting.timeLocation} onChange={(e) => updateSetting({ timeLocation: e.target.value })} />
  232. </SettingListItem>
  233. <SettingListItem paddings="small" title={t('pages.settings.datepicker')} description={t('pages.settings.datepickerDescription')}>
  234. <Select
  235. value={allSetting.datepicker || 'gregorian'}
  236. onChange={(v) => updateSetting({ datepicker: v as 'gregorian' | 'jalalian' })}
  237. style={{ width: '100%' }}
  238. options={DATEPICKER_LIST.map((d) => ({ value: d.value, label: d.name }))}
  239. />
  240. </SettingListItem>
  241. </>
  242. ),
  243. },
  244. {
  245. key: '6',
  246. label: 'LDAP',
  247. children: (
  248. <>
  249. <SettingListItem paddings="small" title="Enable LDAP sync">
  250. <Switch checked={allSetting.ldapEnable} onChange={(v) => updateSetting({ ldapEnable: v })} />
  251. </SettingListItem>
  252. <SettingListItem paddings="small" title="LDAP host">
  253. <Input value={allSetting.ldapHost} onChange={(e) => updateSetting({ ldapHost: e.target.value })} />
  254. </SettingListItem>
  255. <SettingListItem paddings="small" title="LDAP port">
  256. <InputNumber value={allSetting.ldapPort} min={1} max={65535} style={{ width: '100%' }}
  257. onChange={(v) => updateSetting({ ldapPort: Number(v) || 0 })} />
  258. </SettingListItem>
  259. <SettingListItem paddings="small" title="Use TLS (LDAPS)">
  260. <Switch checked={allSetting.ldapUseTLS} onChange={(v) => updateSetting({ ldapUseTLS: v })} />
  261. </SettingListItem>
  262. <SettingListItem paddings="small" title="Bind DN">
  263. <Input value={allSetting.ldapBindDN} onChange={(e) => updateSetting({ ldapBindDN: e.target.value })} />
  264. </SettingListItem>
  265. <SettingListItem
  266. paddings="small"
  267. title={t('password')}
  268. description={allSetting.hasLdapPassword ? 'Configured; leave blank to keep current password.' : 'Not configured.'}
  269. >
  270. <Input.Password
  271. value={allSetting.ldapPassword}
  272. placeholder={allSetting.hasLdapPassword ? 'Configured - enter a new value to replace' : ''}
  273. onChange={(e) => updateSetting({ ldapPassword: e.target.value })}
  274. />
  275. </SettingListItem>
  276. <SettingListItem paddings="small" title="Base DN">
  277. <Input value={allSetting.ldapBaseDN} onChange={(e) => updateSetting({ ldapBaseDN: e.target.value })} />
  278. </SettingListItem>
  279. <SettingListItem paddings="small" title="User filter">
  280. <Input value={allSetting.ldapUserFilter} onChange={(e) => updateSetting({ ldapUserFilter: e.target.value })} />
  281. </SettingListItem>
  282. <SettingListItem paddings="small" title="User attribute (username/email)">
  283. <Input value={allSetting.ldapUserAttr} onChange={(e) => updateSetting({ ldapUserAttr: e.target.value })} />
  284. </SettingListItem>
  285. <SettingListItem paddings="small" title="VLESS flag attribute">
  286. <Input value={allSetting.ldapVlessField} onChange={(e) => updateSetting({ ldapVlessField: e.target.value })} />
  287. </SettingListItem>
  288. <SettingListItem paddings="small" title="Generic flag attribute (optional)" description="If set, overrides VLESS flag — e.g. shadowInactive.">
  289. <Input value={allSetting.ldapFlagField} onChange={(e) => updateSetting({ ldapFlagField: e.target.value })} />
  290. </SettingListItem>
  291. <SettingListItem paddings="small" title="Truthy values" description="Comma-separated; default: true,1,yes,on">
  292. <Input value={allSetting.ldapTruthyValues} onChange={(e) => updateSetting({ ldapTruthyValues: e.target.value })} />
  293. </SettingListItem>
  294. <SettingListItem paddings="small" title="Invert flag" description="Enable when the attribute means disabled (e.g. shadowInactive).">
  295. <Switch checked={allSetting.ldapInvertFlag} onChange={(v) => updateSetting({ ldapInvertFlag: v })} />
  296. </SettingListItem>
  297. <SettingListItem paddings="small" title="Sync schedule" description="Cron-like string, e.g. @every 1m">
  298. <Input value={allSetting.ldapSyncCron} onChange={(e) => updateSetting({ ldapSyncCron: e.target.value })} />
  299. </SettingListItem>
  300. <SettingListItem paddings="small" title="Inbound tags" description="Inbounds that LDAP sync may auto-create or auto-delete clients on.">
  301. <>
  302. <Select
  303. mode="multiple"
  304. value={ldapInboundTagList}
  305. onChange={setLdapInboundTagList}
  306. style={{ width: '100%' }}
  307. options={inboundOptions}
  308. />
  309. {inboundOptions.length === 0 && (
  310. <div className="ldap-no-inbounds">No inbounds found. Create one in Inbounds first.</div>
  311. )}
  312. </>
  313. </SettingListItem>
  314. <SettingListItem paddings="small" title="Auto create clients">
  315. <Switch checked={allSetting.ldapAutoCreate} onChange={(v) => updateSetting({ ldapAutoCreate: v })} />
  316. </SettingListItem>
  317. <SettingListItem paddings="small" title="Auto delete clients">
  318. <Switch checked={allSetting.ldapAutoDelete} onChange={(v) => updateSetting({ ldapAutoDelete: v })} />
  319. </SettingListItem>
  320. <SettingListItem paddings="small" title="Default total (GB)">
  321. <InputNumber value={allSetting.ldapDefaultTotalGB} min={0} style={{ width: '100%' }}
  322. onChange={(v) => updateSetting({ ldapDefaultTotalGB: Number(v) || 0 })} />
  323. </SettingListItem>
  324. <SettingListItem paddings="small" title="Default expiry (days)">
  325. <InputNumber value={allSetting.ldapDefaultExpiryDays} min={0} style={{ width: '100%' }}
  326. onChange={(v) => updateSetting({ ldapDefaultExpiryDays: Number(v) || 0 })} />
  327. </SettingListItem>
  328. <SettingListItem paddings="small" title="Default IP limit">
  329. <InputNumber value={allSetting.ldapDefaultLimitIP} min={0} style={{ width: '100%' }}
  330. onChange={(v) => updateSetting({ ldapDefaultLimitIP: Number(v) || 0 })} />
  331. </SettingListItem>
  332. </>
  333. ),
  334. },
  335. ]} />
  336. );
  337. }