SystemSetting.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import {
  3. Button,
  4. Form,
  5. Row,
  6. Col,
  7. Typography,
  8. Modal,
  9. Banner,
  10. TagInput,
  11. Spin,
  12. Card,
  13. } from '@douyinfe/semi-ui';
  14. const { Text } = Typography;
  15. import {
  16. API,
  17. removeTrailingSlash,
  18. showError,
  19. showSuccess,
  20. toBoolean,
  21. } from '../../helpers';
  22. import axios from 'axios';
  23. import { useTranslation } from 'react-i18next';
  24. const SystemSetting = () => {
  25. const { t } = useTranslation();
  26. let [inputs, setInputs] = useState({
  27. PasswordLoginEnabled: '',
  28. PasswordRegisterEnabled: '',
  29. EmailVerificationEnabled: '',
  30. GitHubOAuthEnabled: '',
  31. GitHubClientId: '',
  32. GitHubClientSecret: '',
  33. 'oidc.enabled': '',
  34. 'oidc.client_id': '',
  35. 'oidc.client_secret': '',
  36. 'oidc.well_known': '',
  37. 'oidc.authorization_endpoint': '',
  38. 'oidc.token_endpoint': '',
  39. 'oidc.user_info_endpoint': '',
  40. Notice: '',
  41. SMTPServer: '',
  42. SMTPPort: '',
  43. SMTPAccount: '',
  44. SMTPFrom: '',
  45. SMTPToken: '',
  46. WorkerUrl: '',
  47. WorkerValidKey: '',
  48. WorkerAllowHttpImageRequestEnabled: '',
  49. Footer: '',
  50. WeChatAuthEnabled: '',
  51. WeChatServerAddress: '',
  52. WeChatServerToken: '',
  53. WeChatAccountQRCodeImageURL: '',
  54. TurnstileCheckEnabled: '',
  55. TurnstileSiteKey: '',
  56. TurnstileSecretKey: '',
  57. RegisterEnabled: '',
  58. EmailDomainRestrictionEnabled: '',
  59. EmailAliasRestrictionEnabled: '',
  60. SMTPSSLEnabled: '',
  61. EmailDomainWhitelist: [],
  62. TelegramOAuthEnabled: '',
  63. TelegramBotToken: '',
  64. TelegramBotName: '',
  65. LinuxDOOAuthEnabled: '',
  66. LinuxDOClientId: '',
  67. LinuxDOClientSecret: '',
  68. ServerAddress: '',
  69. });
  70. const [originInputs, setOriginInputs] = useState({});
  71. const [loading, setLoading] = useState(false);
  72. const [isLoaded, setIsLoaded] = useState(false);
  73. const formApiRef = useRef(null);
  74. const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
  75. const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] =
  76. useState(false);
  77. const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
  78. const [emailToAdd, setEmailToAdd] = useState('');
  79. const getOptions = async () => {
  80. setLoading(true);
  81. const res = await API.get('/api/option/');
  82. const { success, message, data } = res.data;
  83. if (success) {
  84. let newInputs = {};
  85. data.forEach((item) => {
  86. switch (item.key) {
  87. case 'TopupGroupRatio':
  88. item.value = JSON.stringify(JSON.parse(item.value), null, 2);
  89. break;
  90. case 'EmailDomainWhitelist':
  91. setEmailDomainWhitelist(item.value ? item.value.split(',') : []);
  92. break;
  93. case 'PasswordLoginEnabled':
  94. case 'PasswordRegisterEnabled':
  95. case 'EmailVerificationEnabled':
  96. case 'GitHubOAuthEnabled':
  97. case 'WeChatAuthEnabled':
  98. case 'TelegramOAuthEnabled':
  99. case 'RegisterEnabled':
  100. case 'TurnstileCheckEnabled':
  101. case 'EmailDomainRestrictionEnabled':
  102. case 'EmailAliasRestrictionEnabled':
  103. case 'SMTPSSLEnabled':
  104. case 'LinuxDOOAuthEnabled':
  105. case 'oidc.enabled':
  106. case 'WorkerAllowHttpImageRequestEnabled':
  107. item.value = toBoolean(item.value);
  108. break;
  109. case 'Price':
  110. case 'MinTopUp':
  111. item.value = parseFloat(item.value);
  112. break;
  113. default:
  114. break;
  115. }
  116. newInputs[item.key] = item.value;
  117. });
  118. setInputs(newInputs);
  119. setOriginInputs(newInputs);
  120. if (formApiRef.current) {
  121. formApiRef.current.setValues(newInputs);
  122. }
  123. setIsLoaded(true);
  124. } else {
  125. showError(message);
  126. }
  127. setLoading(false);
  128. };
  129. useEffect(() => {
  130. getOptions();
  131. }, []);
  132. const updateOptions = async (options) => {
  133. setLoading(true);
  134. try {
  135. // 分离 checkbox 类型的选项和其他选项
  136. const checkboxOptions = options.filter((opt) =>
  137. opt.key.toLowerCase().endsWith('enabled'),
  138. );
  139. const otherOptions = options.filter(
  140. (opt) => !opt.key.toLowerCase().endsWith('enabled'),
  141. );
  142. // 处理 checkbox 类型的选项
  143. for (const opt of checkboxOptions) {
  144. const res = await API.put('/api/option/', {
  145. key: opt.key,
  146. value: opt.value.toString(),
  147. });
  148. if (!res.data.success) {
  149. showError(res.data.message);
  150. return;
  151. }
  152. }
  153. // 处理其他选项
  154. if (otherOptions.length > 0) {
  155. const requestQueue = otherOptions.map((opt) =>
  156. API.put('/api/option/', {
  157. key: opt.key,
  158. value:
  159. typeof opt.value === 'boolean' ? opt.value.toString() : opt.value,
  160. }),
  161. );
  162. const results = await Promise.all(requestQueue);
  163. // 检查所有请求是否成功
  164. const errorResults = results.filter((res) => !res.data.success);
  165. errorResults.forEach((res) => {
  166. showError(res.data.message);
  167. });
  168. }
  169. showSuccess(t('更新成功'));
  170. // 更新本地状态
  171. const newInputs = { ...inputs };
  172. options.forEach((opt) => {
  173. newInputs[opt.key] = opt.value;
  174. });
  175. setInputs(newInputs);
  176. } catch (error) {
  177. showError(t('更新失败'));
  178. }
  179. setLoading(false);
  180. };
  181. const handleFormChange = (values) => {
  182. setInputs(values);
  183. };
  184. const submitWorker = async () => {
  185. let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
  186. const options = [
  187. { key: 'WorkerUrl', value: WorkerUrl },
  188. {
  189. key: 'WorkerAllowHttpImageRequestEnabled',
  190. value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false',
  191. },
  192. ];
  193. if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
  194. options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
  195. }
  196. await updateOptions(options);
  197. };
  198. const submitServerAddress = async () => {
  199. let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
  200. await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
  201. };
  202. const submitSMTP = async () => {
  203. const options = [];
  204. if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
  205. options.push({ key: 'SMTPServer', value: inputs.SMTPServer });
  206. }
  207. if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
  208. options.push({ key: 'SMTPAccount', value: inputs.SMTPAccount });
  209. }
  210. if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
  211. options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom });
  212. }
  213. if (
  214. originInputs['SMTPPort'] !== inputs.SMTPPort &&
  215. inputs.SMTPPort !== ''
  216. ) {
  217. options.push({ key: 'SMTPPort', value: inputs.SMTPPort });
  218. }
  219. if (
  220. originInputs['SMTPToken'] !== inputs.SMTPToken &&
  221. inputs.SMTPToken !== ''
  222. ) {
  223. options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
  224. }
  225. if (options.length > 0) {
  226. await updateOptions(options);
  227. }
  228. };
  229. const submitEmailDomainWhitelist = async () => {
  230. if (Array.isArray(emailDomainWhitelist)) {
  231. await updateOptions([
  232. {
  233. key: 'EmailDomainWhitelist',
  234. value: emailDomainWhitelist.join(','),
  235. },
  236. ]);
  237. } else {
  238. showError(t('邮箱域名白名单格式不正确'));
  239. }
  240. };
  241. const handleAddEmail = () => {
  242. if (emailToAdd && emailToAdd.trim() !== '') {
  243. const domain = emailToAdd.trim();
  244. // 验证域名格式
  245. const domainRegex =
  246. /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
  247. if (!domainRegex.test(domain)) {
  248. showError(t('邮箱域名格式不正确,请输入有效的域名,如 gmail.com'));
  249. return;
  250. }
  251. // 检查是否已存在
  252. if (emailDomainWhitelist.includes(domain)) {
  253. showError(t('该域名已存在于白名单中'));
  254. return;
  255. }
  256. setEmailDomainWhitelist([...emailDomainWhitelist, domain]);
  257. setEmailToAdd('');
  258. showSuccess(t('已添加到白名单'));
  259. }
  260. };
  261. const submitWeChat = async () => {
  262. const options = [];
  263. if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
  264. options.push({
  265. key: 'WeChatServerAddress',
  266. value: removeTrailingSlash(inputs.WeChatServerAddress),
  267. });
  268. }
  269. if (
  270. originInputs['WeChatAccountQRCodeImageURL'] !==
  271. inputs.WeChatAccountQRCodeImageURL
  272. ) {
  273. options.push({
  274. key: 'WeChatAccountQRCodeImageURL',
  275. value: inputs.WeChatAccountQRCodeImageURL,
  276. });
  277. }
  278. if (
  279. originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
  280. inputs.WeChatServerToken !== ''
  281. ) {
  282. options.push({
  283. key: 'WeChatServerToken',
  284. value: inputs.WeChatServerToken,
  285. });
  286. }
  287. if (options.length > 0) {
  288. await updateOptions(options);
  289. }
  290. };
  291. const submitGitHubOAuth = async () => {
  292. const options = [];
  293. if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
  294. options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId });
  295. }
  296. if (
  297. originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
  298. inputs.GitHubClientSecret !== ''
  299. ) {
  300. options.push({
  301. key: 'GitHubClientSecret',
  302. value: inputs.GitHubClientSecret,
  303. });
  304. }
  305. if (options.length > 0) {
  306. await updateOptions(options);
  307. }
  308. };
  309. const submitOIDCSettings = async () => {
  310. if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') {
  311. if (
  312. !inputs['oidc.well_known'].startsWith('http://') &&
  313. !inputs['oidc.well_known'].startsWith('https://')
  314. ) {
  315. showError(t('Well-Known URL 必须以 http:// 或 https:// 开头'));
  316. return;
  317. }
  318. try {
  319. const res = await axios.create().get(inputs['oidc.well_known']);
  320. inputs['oidc.authorization_endpoint'] =
  321. res.data['authorization_endpoint'];
  322. inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
  323. inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
  324. showSuccess(t('获取 OIDC 配置成功!'));
  325. } catch (err) {
  326. console.error(err);
  327. showError(
  328. t('获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确'),
  329. );
  330. return;
  331. }
  332. }
  333. const options = [];
  334. if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
  335. options.push({
  336. key: 'oidc.well_known',
  337. value: inputs['oidc.well_known'],
  338. });
  339. }
  340. if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
  341. options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] });
  342. }
  343. if (
  344. originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] &&
  345. inputs['oidc.client_secret'] !== ''
  346. ) {
  347. options.push({
  348. key: 'oidc.client_secret',
  349. value: inputs['oidc.client_secret'],
  350. });
  351. }
  352. if (
  353. originInputs['oidc.authorization_endpoint'] !==
  354. inputs['oidc.authorization_endpoint']
  355. ) {
  356. options.push({
  357. key: 'oidc.authorization_endpoint',
  358. value: inputs['oidc.authorization_endpoint'],
  359. });
  360. }
  361. if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
  362. options.push({
  363. key: 'oidc.token_endpoint',
  364. value: inputs['oidc.token_endpoint'],
  365. });
  366. }
  367. if (
  368. originInputs['oidc.user_info_endpoint'] !==
  369. inputs['oidc.user_info_endpoint']
  370. ) {
  371. options.push({
  372. key: 'oidc.user_info_endpoint',
  373. value: inputs['oidc.user_info_endpoint'],
  374. });
  375. }
  376. if (options.length > 0) {
  377. await updateOptions(options);
  378. }
  379. };
  380. const submitTelegramSettings = async () => {
  381. const options = [
  382. { key: 'TelegramBotToken', value: inputs.TelegramBotToken },
  383. { key: 'TelegramBotName', value: inputs.TelegramBotName },
  384. ];
  385. await updateOptions(options);
  386. };
  387. const submitTurnstile = async () => {
  388. const options = [];
  389. if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
  390. options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey });
  391. }
  392. if (
  393. originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
  394. inputs.TurnstileSecretKey !== ''
  395. ) {
  396. options.push({
  397. key: 'TurnstileSecretKey',
  398. value: inputs.TurnstileSecretKey,
  399. });
  400. }
  401. if (options.length > 0) {
  402. await updateOptions(options);
  403. }
  404. };
  405. const submitLinuxDOOAuth = async () => {
  406. const options = [];
  407. if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
  408. options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId });
  409. }
  410. if (
  411. originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret &&
  412. inputs.LinuxDOClientSecret !== ''
  413. ) {
  414. options.push({
  415. key: 'LinuxDOClientSecret',
  416. value: inputs.LinuxDOClientSecret,
  417. });
  418. }
  419. if (options.length > 0) {
  420. await updateOptions(options);
  421. }
  422. };
  423. const handleCheckboxChange = async (optionKey, event) => {
  424. const value = event.target.checked;
  425. if (optionKey === 'PasswordLoginEnabled' && !value) {
  426. setShowPasswordLoginConfirmModal(true);
  427. } else {
  428. await updateOptions([{ key: optionKey, value }]);
  429. }
  430. if (optionKey === 'LinuxDOOAuthEnabled') {
  431. setLinuxDOOAuthEnabled(value);
  432. }
  433. };
  434. const handlePasswordLoginConfirm = async () => {
  435. await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]);
  436. setShowPasswordLoginConfirmModal(false);
  437. };
  438. return (
  439. <div>
  440. {isLoaded ? (
  441. <Form
  442. initValues={inputs}
  443. onValueChange={handleFormChange}
  444. getFormApi={(api) => (formApiRef.current = api)}
  445. >
  446. {({ formState, values, formApi }) => (
  447. <div
  448. style={{
  449. display: 'flex',
  450. flexDirection: 'column',
  451. gap: '10px',
  452. marginTop: '10px',
  453. }}
  454. >
  455. <Card>
  456. <Form.Section text={t('通用设置')}>
  457. <Row
  458. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  459. >
  460. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  461. <Form.Input
  462. field='ServerAddress'
  463. label={t('服务器地址')}
  464. placeholder='https://yourdomain.com'
  465. extraText={t('该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置')}
  466. />
  467. </Col>
  468. </Row>
  469. <Button onClick={submitServerAddress}>{t('更新服务器地址')}</Button>
  470. </Form.Section>
  471. </Card>
  472. <Card>
  473. <Form.Section text={t('代理设置')}>
  474. <Text>
  475. (支持{' '}
  476. <a
  477. href='https://github.com/Calcium-Ion/new-api-worker'
  478. target='_blank'
  479. rel='noreferrer'
  480. >
  481. new-api-worker
  482. </a>
  483. </Text>
  484. <Row
  485. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  486. >
  487. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  488. <Form.Input
  489. field='WorkerUrl'
  490. label={t('Worker地址')}
  491. placeholder='例如:https://workername.yourdomain.workers.dev'
  492. />
  493. </Col>
  494. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  495. <Form.Input
  496. field='WorkerValidKey'
  497. label={t('Worker密钥')}
  498. placeholder='敏感信息不会发送到前端显示'
  499. type='password'
  500. />
  501. </Col>
  502. </Row>
  503. <Form.Checkbox
  504. field='WorkerAllowHttpImageRequestEnabled'
  505. noLabel
  506. >
  507. {t('允许 HTTP 协议图片请求(适用于自部署代理)')}
  508. </Form.Checkbox>
  509. <Button onClick={submitWorker}>{t('更新Worker设置')}</Button>
  510. </Form.Section>
  511. </Card>
  512. <Card>
  513. <Form.Section text={t('配置登录注册')}>
  514. <Row
  515. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  516. >
  517. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  518. <Form.Checkbox
  519. field='PasswordLoginEnabled'
  520. noLabel
  521. onChange={(e) =>
  522. handleCheckboxChange('PasswordLoginEnabled', e)
  523. }
  524. >
  525. {t('允许通过密码进行登录')}
  526. </Form.Checkbox>
  527. <Form.Checkbox
  528. field='PasswordRegisterEnabled'
  529. noLabel
  530. onChange={(e) =>
  531. handleCheckboxChange('PasswordRegisterEnabled', e)
  532. }
  533. >
  534. {t('允许通过密码进行注册')}
  535. </Form.Checkbox>
  536. <Form.Checkbox
  537. field='EmailVerificationEnabled'
  538. noLabel
  539. onChange={(e) =>
  540. handleCheckboxChange('EmailVerificationEnabled', e)
  541. }
  542. >
  543. {t('通过密码注册时需要进行邮箱验证')}
  544. </Form.Checkbox>
  545. <Form.Checkbox
  546. field='RegisterEnabled'
  547. noLabel
  548. onChange={(e) =>
  549. handleCheckboxChange('RegisterEnabled', e)
  550. }
  551. >
  552. {t('允许新用户注册')}
  553. </Form.Checkbox>
  554. <Form.Checkbox
  555. field='TurnstileCheckEnabled'
  556. noLabel
  557. onChange={(e) =>
  558. handleCheckboxChange('TurnstileCheckEnabled', e)
  559. }
  560. >
  561. {t('允许 Turnstile 用户校验')}
  562. </Form.Checkbox>
  563. </Col>
  564. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  565. <Form.Checkbox
  566. field='GitHubOAuthEnabled'
  567. noLabel
  568. onChange={(e) =>
  569. handleCheckboxChange('GitHubOAuthEnabled', e)
  570. }
  571. >
  572. {t('允许通过 GitHub 账户登录 & 注册')}
  573. </Form.Checkbox>
  574. <Form.Checkbox
  575. field='LinuxDOOAuthEnabled'
  576. noLabel
  577. onChange={(e) =>
  578. handleCheckboxChange('LinuxDOOAuthEnabled', e)
  579. }
  580. >
  581. {t('允许通过 Linux DO 账户登录 & 注册')}
  582. </Form.Checkbox>
  583. <Form.Checkbox
  584. field='WeChatAuthEnabled'
  585. noLabel
  586. onChange={(e) =>
  587. handleCheckboxChange('WeChatAuthEnabled', e)
  588. }
  589. >
  590. {t('允许通过微信登录 & 注册')}
  591. </Form.Checkbox>
  592. <Form.Checkbox
  593. field='TelegramOAuthEnabled'
  594. noLabel
  595. onChange={(e) =>
  596. handleCheckboxChange('TelegramOAuthEnabled', e)
  597. }
  598. >
  599. {t('允许通过 Telegram 进行登录')}
  600. </Form.Checkbox>
  601. <Form.Checkbox
  602. field="['oidc.enabled']"
  603. noLabel
  604. onChange={(e) =>
  605. handleCheckboxChange('oidc.enabled', e)
  606. }
  607. >
  608. {t('允许通过 OIDC 进行登录')}
  609. </Form.Checkbox>
  610. </Col>
  611. </Row>
  612. </Form.Section>
  613. </Card>
  614. <Card>
  615. <Form.Section text={t('配置邮箱域名白名单')}>
  616. <Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text>
  617. <Row
  618. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  619. >
  620. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  621. <Form.Checkbox
  622. field='EmailDomainRestrictionEnabled'
  623. noLabel
  624. onChange={(e) =>
  625. handleCheckboxChange(
  626. 'EmailDomainRestrictionEnabled',
  627. e,
  628. )
  629. }
  630. >
  631. 启用邮箱域名白名单
  632. </Form.Checkbox>
  633. </Col>
  634. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  635. <Form.Checkbox
  636. field='EmailAliasRestrictionEnabled'
  637. noLabel
  638. onChange={(e) =>
  639. handleCheckboxChange(
  640. 'EmailAliasRestrictionEnabled',
  641. e,
  642. )
  643. }
  644. >
  645. 启用邮箱别名限制
  646. </Form.Checkbox>
  647. </Col>
  648. </Row>
  649. <TagInput
  650. value={emailDomainWhitelist}
  651. onChange={setEmailDomainWhitelist}
  652. placeholder={t('输入域名后回车')}
  653. style={{ width: '100%', marginTop: 16 }}
  654. />
  655. <Form.Input
  656. placeholder={t('输入要添加的邮箱域名')}
  657. value={emailToAdd}
  658. onChange={(value) => setEmailToAdd(value)}
  659. style={{ marginTop: 16 }}
  660. suffix={
  661. <Button
  662. theme='solid'
  663. type='primary'
  664. onClick={handleAddEmail}
  665. >
  666. {t('添加')}
  667. </Button>
  668. }
  669. onEnterPress={handleAddEmail}
  670. />
  671. <Button
  672. onClick={submitEmailDomainWhitelist}
  673. style={{ marginTop: 10 }}
  674. >
  675. {t('保存邮箱域名白名单设置')}
  676. </Button>
  677. </Form.Section>
  678. </Card>
  679. <Card>
  680. <Form.Section text={t('配置 SMTP')}>
  681. <Text>{t('用以支持系统的邮件发送')}</Text>
  682. <Row
  683. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  684. >
  685. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  686. <Form.Input field='SMTPServer' label={t('SMTP 服务器地址')} />
  687. </Col>
  688. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  689. <Form.Input field='SMTPPort' label={t('SMTP 端口')} />
  690. </Col>
  691. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  692. <Form.Input field='SMTPAccount' label={t('SMTP 账户')} />
  693. </Col>
  694. </Row>
  695. <Row
  696. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  697. style={{ marginTop: 16 }}
  698. >
  699. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  700. <Form.Input field='SMTPFrom' label={t('SMTP 发送者邮箱')} />
  701. </Col>
  702. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  703. <Form.Input
  704. field='SMTPToken'
  705. label={t('SMTP 访问凭证')}
  706. type='password'
  707. placeholder='敏感信息不会发送到前端显示'
  708. />
  709. </Col>
  710. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  711. <Form.Checkbox
  712. field='SMTPSSLEnabled'
  713. noLabel
  714. onChange={(e) =>
  715. handleCheckboxChange('SMTPSSLEnabled', e)
  716. }
  717. >
  718. {t('启用SMTP SSL')}
  719. </Form.Checkbox>
  720. </Col>
  721. </Row>
  722. <Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button>
  723. </Form.Section>
  724. </Card>
  725. <Card>
  726. <Form.Section text={t('配置 OIDC')}>
  727. <Text>
  728. {t('用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP')}
  729. </Text>
  730. <Banner
  731. type='info'
  732. description={`${t('主页链接填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('重定向 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/oidc`}
  733. style={{ marginBottom: 20, marginTop: 16 }}
  734. />
  735. <Text>
  736. {t('若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置')}
  737. </Text>
  738. <Row
  739. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  740. >
  741. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  742. <Form.Input
  743. field="['oidc.well_known']"
  744. label={t('Well-Known URL')}
  745. placeholder={t('请输入 OIDC 的 Well-Known URL')}
  746. />
  747. </Col>
  748. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  749. <Form.Input
  750. field="['oidc.client_id']"
  751. label={t('Client ID')}
  752. placeholder={t('输入 OIDC 的 Client ID')}
  753. />
  754. </Col>
  755. </Row>
  756. <Row
  757. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  758. >
  759. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  760. <Form.Input
  761. field="['oidc.client_secret']"
  762. label={t('Client Secret')}
  763. type='password'
  764. placeholder={t('敏感信息不会发送到前端显示')}
  765. />
  766. </Col>
  767. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  768. <Form.Input
  769. field="['oidc.authorization_endpoint']"
  770. label={t('Authorization Endpoint')}
  771. placeholder={t('输入 OIDC 的 Authorization Endpoint')}
  772. />
  773. </Col>
  774. </Row>
  775. <Row
  776. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  777. >
  778. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  779. <Form.Input
  780. field="['oidc.token_endpoint']"
  781. label={t('Token Endpoint')}
  782. placeholder={t('输入 OIDC 的 Token Endpoint')}
  783. />
  784. </Col>
  785. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  786. <Form.Input
  787. field="['oidc.user_info_endpoint']"
  788. label={t('User Info Endpoint')}
  789. placeholder={t('输入 OIDC 的 Userinfo Endpoint')}
  790. />
  791. </Col>
  792. </Row>
  793. <Button onClick={submitOIDCSettings}>{t('保存 OIDC 设置')}</Button>
  794. </Form.Section>
  795. </Card>
  796. <Card>
  797. <Form.Section text={t('配置 GitHub OAuth App')}>
  798. <Text>{t('用以支持通过 GitHub 进行登录注册')}</Text>
  799. <Banner
  800. type='info'
  801. description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/github`}
  802. style={{ marginBottom: 20, marginTop: 16 }}
  803. />
  804. <Row
  805. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  806. >
  807. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  808. <Form.Input
  809. field='GitHubClientId'
  810. label={t('GitHub Client ID')}
  811. />
  812. </Col>
  813. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  814. <Form.Input
  815. field='GitHubClientSecret'
  816. label={t('GitHub Client Secret')}
  817. type='password'
  818. placeholder={t('敏感信息不会发送到前端显示')}
  819. />
  820. </Col>
  821. </Row>
  822. <Button onClick={submitGitHubOAuth}>
  823. {t('保存 GitHub OAuth 设置')}
  824. </Button>
  825. </Form.Section>
  826. </Card>
  827. <Card>
  828. <Form.Section text={t('配置 Linux DO OAuth')}>
  829. <Text>
  830. {t('用以支持通过 Linux DO 进行登录注册')}
  831. <a
  832. href='https://connect.linux.do/'
  833. target='_blank'
  834. rel='noreferrer'
  835. style={{
  836. display: 'inline-block',
  837. marginLeft: 4,
  838. marginRight: 4,
  839. }}
  840. >
  841. {t('点击此处')}
  842. </a>
  843. {t('管理你的 LinuxDO OAuth App')}
  844. </Text>
  845. <Banner
  846. type='info'
  847. description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/linuxdo`}
  848. style={{ marginBottom: 20, marginTop: 16 }}
  849. />
  850. <Row
  851. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  852. >
  853. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  854. <Form.Input
  855. field='LinuxDOClientId'
  856. label={t('Linux DO Client ID')}
  857. placeholder={t('输入你注册的 LinuxDO OAuth APP 的 ID')}
  858. />
  859. </Col>
  860. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  861. <Form.Input
  862. field='LinuxDOClientSecret'
  863. label={t('Linux DO Client Secret')}
  864. type='password'
  865. placeholder={t('敏感信息不会发送到前端显示')}
  866. />
  867. </Col>
  868. </Row>
  869. <Button onClick={submitLinuxDOOAuth}>
  870. {t('保存 Linux DO OAuth 设置')}
  871. </Button>
  872. </Form.Section>
  873. </Card>
  874. <Card>
  875. <Form.Section text={t('配置 WeChat Server')}>
  876. <Text>{t('用以支持通过微信进行登录注册')}</Text>
  877. <Row
  878. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  879. >
  880. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  881. <Form.Input
  882. field='WeChatServerAddress'
  883. label={t('WeChat Server 服务器地址')}
  884. />
  885. </Col>
  886. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  887. <Form.Input
  888. field='WeChatServerToken'
  889. label={t('WeChat Server 访问凭证')}
  890. type='password'
  891. placeholder={t('敏感信息不会发送到前端显示')}
  892. />
  893. </Col>
  894. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  895. <Form.Input
  896. field='WeChatAccountQRCodeImageURL'
  897. label={t('微信公众号二维码图片链接')}
  898. />
  899. </Col>
  900. </Row>
  901. <Button onClick={submitWeChat}>
  902. {t('保存 WeChat Server 设置')}
  903. </Button>
  904. </Form.Section>
  905. </Card>
  906. <Card>
  907. <Form.Section text={t('配置 Telegram 登录')}>
  908. <Text>{t('用以支持通过 Telegram 进行登录注册')}</Text>
  909. <Row
  910. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  911. >
  912. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  913. <Form.Input
  914. field='TelegramBotToken'
  915. label={t('Telegram Bot Token')}
  916. placeholder={t('敏感信息不会发送到前端显示')}
  917. type='password'
  918. />
  919. </Col>
  920. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  921. <Form.Input
  922. field='TelegramBotName'
  923. label={t('Telegram Bot 名称')}
  924. />
  925. </Col>
  926. </Row>
  927. <Button onClick={submitTelegramSettings}>
  928. {t('保存 Telegram 登录设置')}
  929. </Button>
  930. </Form.Section>
  931. </Card>
  932. <Card>
  933. <Form.Section text={t('配置 Turnstile')}>
  934. <Text>{t('用以支持用户校验')}</Text>
  935. <Row
  936. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  937. >
  938. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  939. <Form.Input
  940. field='TurnstileSiteKey'
  941. label={t('Turnstile Site Key')}
  942. />
  943. </Col>
  944. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  945. <Form.Input
  946. field='TurnstileSecretKey'
  947. label={t('Turnstile Secret Key')}
  948. type='password'
  949. placeholder={t('敏感信息不会发送到前端显示')}
  950. />
  951. </Col>
  952. </Row>
  953. <Button onClick={submitTurnstile}>{t('保存 Turnstile 设置')}</Button>
  954. </Form.Section>
  955. </Card>
  956. <Modal
  957. title={t('确认取消密码登录')}
  958. visible={showPasswordLoginConfirmModal}
  959. onOk={handlePasswordLoginConfirm}
  960. onCancel={() => {
  961. setShowPasswordLoginConfirmModal(false);
  962. formApiRef.current.setValue('PasswordLoginEnabled', true);
  963. }}
  964. okText={t('确认')}
  965. cancelText={t('取消')}
  966. >
  967. <p>{t('您确定要取消密码登录功能吗?这可能会影响用户的登录方式。')}</p>
  968. </Modal>
  969. </div>
  970. )}
  971. </Form>
  972. ) : (
  973. <div
  974. style={{
  975. display: 'flex',
  976. justifyContent: 'center',
  977. alignItems: 'center',
  978. height: '100vh',
  979. }}
  980. >
  981. <Spin size='large' />
  982. </div>
  983. )}
  984. </div>
  985. );
  986. };
  987. export default SystemSetting;