SystemSetting.js 35 KB

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