SystemSetting.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. import React, { useEffect, useState } from 'react';
  2. import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react';
  3. import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
  4. const SystemSetting = () => {
  5. let [inputs, setInputs] = useState({
  6. PasswordLoginEnabled: '',
  7. PasswordRegisterEnabled: '',
  8. EmailVerificationEnabled: '',
  9. GitHubOAuthEnabled: '',
  10. GitHubClientId: '',
  11. GitHubClientSecret: '',
  12. Notice: '',
  13. SMTPServer: '',
  14. SMTPPort: '',
  15. SMTPAccount: '',
  16. SMTPFrom: '',
  17. SMTPToken: '',
  18. ServerAddress: '',
  19. EpayId: '',
  20. EpayKey: '',
  21. Price: 7.3,
  22. TopupGroupRatio: '',
  23. PayAddress: '',
  24. Footer: '',
  25. WeChatAuthEnabled: '',
  26. WeChatServerAddress: '',
  27. WeChatServerToken: '',
  28. WeChatAccountQRCodeImageURL: '',
  29. TurnstileCheckEnabled: '',
  30. TurnstileSiteKey: '',
  31. TurnstileSecretKey: '',
  32. RegisterEnabled: '',
  33. EmailDomainRestrictionEnabled: '',
  34. EmailDomainWhitelist: '',
  35. // telegram login
  36. TelegramOAuthEnabled: '',
  37. TelegramBotToken: '',
  38. TelegramBotName: '',
  39. });
  40. const [originInputs, setOriginInputs] = useState({});
  41. let [loading, setLoading] = useState(false);
  42. const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
  43. const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
  44. const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
  45. const getOptions = async () => {
  46. const res = await API.get('/api/option/');
  47. const { success, message, data } = res.data;
  48. if (success) {
  49. let newInputs = {};
  50. data.forEach((item) => {
  51. if (item.key === 'TopupGroupRatio') {
  52. item.value = JSON.stringify(JSON.parse(item.value), null, 2);
  53. }
  54. newInputs[item.key] = item.value;
  55. });
  56. setInputs({
  57. ...newInputs,
  58. EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
  59. });
  60. setOriginInputs(newInputs);
  61. setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
  62. return { key: item, text: item, value: item };
  63. }));
  64. } else {
  65. showError(message);
  66. }
  67. };
  68. useEffect(() => {
  69. getOptions().then();
  70. }, []);
  71. const updateOption = async (key, value) => {
  72. setLoading(true);
  73. switch (key) {
  74. case 'PasswordLoginEnabled':
  75. case 'PasswordRegisterEnabled':
  76. case 'EmailVerificationEnabled':
  77. case 'GitHubOAuthEnabled':
  78. case 'WeChatAuthEnabled':
  79. case 'TelegramOAuthEnabled':
  80. case 'TurnstileCheckEnabled':
  81. case 'EmailDomainRestrictionEnabled':
  82. case 'RegisterEnabled':
  83. value = inputs[key] === 'true' ? 'false' : 'true';
  84. break;
  85. default:
  86. break;
  87. }
  88. const res = await API.put('/api/option/', {
  89. key,
  90. value
  91. });
  92. const { success, message } = res.data;
  93. if (success) {
  94. if (key === 'EmailDomainWhitelist') {
  95. value = value.split(',');
  96. }
  97. if (key === 'Price') {
  98. value = parseFloat(value);
  99. }
  100. setInputs((inputs) => ({
  101. ...inputs, [key]: value
  102. }));
  103. } else {
  104. showError(message);
  105. }
  106. setLoading(false);
  107. };
  108. const handleInputChange = async (e, { name, value }) => {
  109. if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
  110. // block disabling password login
  111. setShowPasswordWarningModal(true);
  112. return;
  113. }
  114. if (
  115. name === 'Notice' ||
  116. name.startsWith('SMTP') ||
  117. name === 'ServerAddress' ||
  118. name === 'EpayId' ||
  119. name === 'EpayKey' ||
  120. name === 'Price' ||
  121. name === 'PayAddress' ||
  122. name === 'GitHubClientId' ||
  123. name === 'GitHubClientSecret' ||
  124. name === 'WeChatServerAddress' ||
  125. name === 'WeChatServerToken' ||
  126. name === 'WeChatAccountQRCodeImageURL' ||
  127. name === 'TurnstileSiteKey' ||
  128. name === 'TurnstileSecretKey' ||
  129. name === 'EmailDomainWhitelist' ||
  130. name === 'TopupGroupRatio'
  131. ) {
  132. setInputs((inputs) => ({ ...inputs, [name]: value }));
  133. } else {
  134. await updateOption(name, value);
  135. }
  136. };
  137. const submitServerAddress = async () => {
  138. let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
  139. await updateOption('ServerAddress', ServerAddress);
  140. };
  141. const submitPayAddress = async () => {
  142. if (inputs.ServerAddress === '') {
  143. showError('请先填写服务器地址');
  144. return
  145. }
  146. if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
  147. if (!verifyJSON(inputs.TopupGroupRatio)) {
  148. showError('充值分组倍率不是合法的 JSON 字符串');
  149. return;
  150. }
  151. await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
  152. }
  153. let PayAddress = removeTrailingSlash(inputs.PayAddress);
  154. await updateOption('PayAddress', PayAddress);
  155. if (inputs.EpayId !== '') {
  156. await updateOption('EpayId', inputs.EpayId);
  157. }
  158. if (inputs.EpayKey !== '') {
  159. await updateOption('EpayKey', inputs.EpayKey);
  160. }
  161. await updateOption('Price', "" + inputs.Price);
  162. };
  163. const submitSMTP = async () => {
  164. if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
  165. await updateOption('SMTPServer', inputs.SMTPServer);
  166. }
  167. if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
  168. await updateOption('SMTPAccount', inputs.SMTPAccount);
  169. }
  170. if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
  171. await updateOption('SMTPFrom', inputs.SMTPFrom);
  172. }
  173. if (
  174. originInputs['SMTPPort'] !== inputs.SMTPPort &&
  175. inputs.SMTPPort !== ''
  176. ) {
  177. await updateOption('SMTPPort', inputs.SMTPPort);
  178. }
  179. if (
  180. originInputs['SMTPToken'] !== inputs.SMTPToken &&
  181. inputs.SMTPToken !== ''
  182. ) {
  183. await updateOption('SMTPToken', inputs.SMTPToken);
  184. }
  185. };
  186. const submitEmailDomainWhitelist = async () => {
  187. if (
  188. originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
  189. inputs.SMTPToken !== ''
  190. ) {
  191. await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
  192. }
  193. };
  194. const submitWeChat = async () => {
  195. if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
  196. await updateOption(
  197. 'WeChatServerAddress',
  198. removeTrailingSlash(inputs.WeChatServerAddress)
  199. );
  200. }
  201. if (
  202. originInputs['WeChatAccountQRCodeImageURL'] !==
  203. inputs.WeChatAccountQRCodeImageURL
  204. ) {
  205. await updateOption(
  206. 'WeChatAccountQRCodeImageURL',
  207. inputs.WeChatAccountQRCodeImageURL
  208. );
  209. }
  210. if (
  211. originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
  212. inputs.WeChatServerToken !== ''
  213. ) {
  214. await updateOption('WeChatServerToken', inputs.WeChatServerToken);
  215. }
  216. };
  217. const submitGitHubOAuth = async () => {
  218. if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
  219. await updateOption('GitHubClientId', inputs.GitHubClientId);
  220. }
  221. if (
  222. originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
  223. inputs.GitHubClientSecret !== ''
  224. ) {
  225. await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
  226. }
  227. };
  228. const submitTelegramSettings = async () => {
  229. await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
  230. await updateOption('TelegramBotToken', inputs.TelegramBotToken);
  231. await updateOption('TelegramBotName', inputs.TelegramBotName);
  232. };
  233. const submitTurnstile = async () => {
  234. if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
  235. await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
  236. }
  237. if (
  238. originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
  239. inputs.TurnstileSecretKey !== ''
  240. ) {
  241. await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
  242. }
  243. };
  244. const submitNewRestrictedDomain = () => {
  245. const localDomainList = inputs.EmailDomainWhitelist;
  246. if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
  247. setRestrictedDomainInput('');
  248. setInputs({
  249. ...inputs,
  250. EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
  251. });
  252. setEmailDomainWhitelist([...EmailDomainWhitelist, {
  253. key: restrictedDomainInput,
  254. text: restrictedDomainInput,
  255. value: restrictedDomainInput,
  256. }]);
  257. }
  258. }
  259. return (
  260. <Grid columns={1}>
  261. <Grid.Column>
  262. <Form loading={loading}>
  263. <Header as='h3'>通用设置</Header>
  264. <Form.Group widths='equal'>
  265. <Form.Input
  266. label='服务器地址'
  267. placeholder='例如:https://yourdomain.com'
  268. value={inputs.ServerAddress}
  269. name='ServerAddress'
  270. onChange={handleInputChange}
  271. />
  272. </Form.Group>
  273. <Form.Button onClick={submitServerAddress}>
  274. 更新服务器地址
  275. </Form.Button>
  276. <Divider />
  277. <Header as='h3'>支付设置(当前仅支持易支付接口,使用上方服务器地址作为回调地址!)</Header>
  278. <Form.Group widths='equal'>
  279. <Form.Input
  280. label='支付地址,不填写则不启用在线支付'
  281. placeholder='例如:https://yourdomain.com'
  282. value={inputs.PayAddress}
  283. name='PayAddress'
  284. onChange={handleInputChange}
  285. />
  286. <Form.Input
  287. label='易支付商户ID'
  288. placeholder='例如:0001'
  289. value={inputs.EpayId}
  290. name='EpayId'
  291. onChange={handleInputChange}
  292. />
  293. <Form.Input
  294. label='易支付商户密钥'
  295. placeholder='例如:dejhfueqhujasjmndbjkqaw'
  296. value={inputs.EpayKey}
  297. name='EpayKey'
  298. onChange={handleInputChange}
  299. />
  300. <Form.Input
  301. label='充值价格(x元/美金)'
  302. placeholder='例如:7,就是7元/美金'
  303. value={inputs.Price}
  304. name='Price'
  305. min={0}
  306. onChange={handleInputChange}
  307. />
  308. </Form.Group>
  309. <Form.Group widths='equal'>
  310. <Form.TextArea
  311. label='充值分组倍率'
  312. name='TopupGroupRatio'
  313. onChange={handleInputChange}
  314. style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
  315. autoComplete='new-password'
  316. value={inputs.TopupGroupRatio}
  317. placeholder='为一个 JSON 文本,键为组名称,值为倍率'
  318. />
  319. </Form.Group>
  320. <Form.Button onClick={submitPayAddress}>
  321. 更新支付设置
  322. </Form.Button>
  323. <Divider />
  324. <Header as='h3'>配置登录注册</Header>
  325. <Form.Group inline>
  326. <Form.Checkbox
  327. checked={inputs.PasswordLoginEnabled === 'true'}
  328. label='允许通过密码进行登录'
  329. name='PasswordLoginEnabled'
  330. onChange={handleInputChange}
  331. />
  332. {
  333. showPasswordWarningModal &&
  334. <Modal
  335. open={showPasswordWarningModal}
  336. onClose={() => setShowPasswordWarningModal(false)}
  337. size={'tiny'}
  338. style={{ maxWidth: '450px' }}
  339. >
  340. <Modal.Header>警告</Modal.Header>
  341. <Modal.Content>
  342. <p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p>
  343. </Modal.Content>
  344. <Modal.Actions>
  345. <Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
  346. <Button
  347. color='yellow'
  348. onClick={async () => {
  349. setShowPasswordWarningModal(false);
  350. await updateOption('PasswordLoginEnabled', 'false');
  351. }}
  352. >
  353. 确定
  354. </Button>
  355. </Modal.Actions>
  356. </Modal>
  357. }
  358. <Form.Checkbox
  359. checked={inputs.PasswordRegisterEnabled === 'true'}
  360. label='允许通过密码进行注册'
  361. name='PasswordRegisterEnabled'
  362. onChange={handleInputChange}
  363. />
  364. <Form.Checkbox
  365. checked={inputs.EmailVerificationEnabled === 'true'}
  366. label='通过密码注册时需要进行邮箱验证'
  367. name='EmailVerificationEnabled'
  368. onChange={handleInputChange}
  369. />
  370. <Form.Checkbox
  371. checked={inputs.GitHubOAuthEnabled === 'true'}
  372. label='允许通过 GitHub 账户登录 & 注册'
  373. name='GitHubOAuthEnabled'
  374. onChange={handleInputChange}
  375. />
  376. <Form.Checkbox
  377. checked={inputs.WeChatAuthEnabled === 'true'}
  378. label='允许通过微信登录 & 注册'
  379. name='WeChatAuthEnabled'
  380. onChange={handleInputChange}
  381. />
  382. <Form.Checkbox
  383. checked={inputs.TelegramOAuthEnabled === 'true'}
  384. label='允许通过 Telegram 进行登录'
  385. name='TelegramOAuthEnabled'
  386. onChange={handleInputChange}
  387. />
  388. </Form.Group>
  389. <Form.Group inline>
  390. <Form.Checkbox
  391. checked={inputs.RegisterEnabled === 'true'}
  392. label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
  393. name='RegisterEnabled'
  394. onChange={handleInputChange}
  395. />
  396. <Form.Checkbox
  397. checked={inputs.TurnstileCheckEnabled === 'true'}
  398. label='启用 Turnstile 用户校验'
  399. name='TurnstileCheckEnabled'
  400. onChange={handleInputChange}
  401. />
  402. </Form.Group>
  403. <Divider />
  404. <Header as='h3'>
  405. 配置邮箱域名白名单
  406. <Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
  407. </Header>
  408. <Form.Group widths={3}>
  409. <Form.Checkbox
  410. label='启用邮箱域名白名单'
  411. name='EmailDomainRestrictionEnabled'
  412. onChange={handleInputChange}
  413. checked={inputs.EmailDomainRestrictionEnabled === 'true'}
  414. />
  415. </Form.Group>
  416. <Form.Group widths={2}>
  417. <Form.Dropdown
  418. label='允许的邮箱域名'
  419. placeholder='允许的邮箱域名'
  420. name='EmailDomainWhitelist'
  421. required
  422. fluid
  423. multiple
  424. selection
  425. onChange={handleInputChange}
  426. value={inputs.EmailDomainWhitelist}
  427. autoComplete='new-password'
  428. options={EmailDomainWhitelist}
  429. />
  430. <Form.Input
  431. label='添加新的允许的邮箱域名'
  432. action={
  433. <Button type='button' onClick={() => {
  434. submitNewRestrictedDomain();
  435. }}>填入</Button>
  436. }
  437. onKeyDown={(e) => {
  438. if (e.key === 'Enter') {
  439. submitNewRestrictedDomain();
  440. }
  441. }}
  442. autoComplete='new-password'
  443. placeholder='输入新的允许的邮箱域名'
  444. value={restrictedDomainInput}
  445. onChange={(e, { value }) => {
  446. setRestrictedDomainInput(value);
  447. }}
  448. />
  449. </Form.Group>
  450. <Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
  451. <Divider />
  452. <Header as='h3'>
  453. 配置 SMTP
  454. <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
  455. </Header>
  456. <Form.Group widths={3}>
  457. <Form.Input
  458. label='SMTP 服务器地址'
  459. name='SMTPServer'
  460. onChange={handleInputChange}
  461. autoComplete='new-password'
  462. value={inputs.SMTPServer}
  463. placeholder='例如:smtp.qq.com'
  464. />
  465. <Form.Input
  466. label='SMTP 端口'
  467. name='SMTPPort'
  468. onChange={handleInputChange}
  469. autoComplete='new-password'
  470. value={inputs.SMTPPort}
  471. placeholder='默认: 587'
  472. />
  473. <Form.Input
  474. label='SMTP 账户'
  475. name='SMTPAccount'
  476. onChange={handleInputChange}
  477. autoComplete='new-password'
  478. value={inputs.SMTPAccount}
  479. placeholder='通常是邮箱地址'
  480. />
  481. </Form.Group>
  482. <Form.Group widths={3}>
  483. <Form.Input
  484. label='SMTP 发送者邮箱'
  485. name='SMTPFrom'
  486. onChange={handleInputChange}
  487. autoComplete='new-password'
  488. value={inputs.SMTPFrom}
  489. placeholder='通常和邮箱地址保持一致'
  490. />
  491. <Form.Input
  492. label='SMTP 访问凭证'
  493. name='SMTPToken'
  494. onChange={handleInputChange}
  495. type='password'
  496. autoComplete='new-password'
  497. checked={inputs.RegisterEnabled === 'true'}
  498. placeholder='敏感信息不会发送到前端显示'
  499. />
  500. </Form.Group>
  501. <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
  502. <Divider />
  503. <Header as='h3'>
  504. 配置 GitHub OAuth App
  505. <Header.Subheader>
  506. 用以支持通过 GitHub 进行登录注册,
  507. <a href='https://github.com/settings/developers' target='_blank'>
  508. 点击此处
  509. </a>
  510. 管理你的 GitHub OAuth App
  511. </Header.Subheader>
  512. </Header>
  513. <Message>
  514. Homepage URL 填 <code>{inputs.ServerAddress}</code>
  515. ,Authorization callback URL 填{' '}
  516. <code>{`${inputs.ServerAddress}/oauth/github`}</code>
  517. </Message>
  518. <Form.Group widths={3}>
  519. <Form.Input
  520. label='GitHub Client ID'
  521. name='GitHubClientId'
  522. onChange={handleInputChange}
  523. autoComplete='new-password'
  524. value={inputs.GitHubClientId}
  525. placeholder='输入你注册的 GitHub OAuth APP 的 ID'
  526. />
  527. <Form.Input
  528. label='GitHub Client Secret'
  529. name='GitHubClientSecret'
  530. onChange={handleInputChange}
  531. type='password'
  532. autoComplete='new-password'
  533. value={inputs.GitHubClientSecret}
  534. placeholder='敏感信息不会发送到前端显示'
  535. />
  536. </Form.Group>
  537. <Form.Button onClick={submitGitHubOAuth}>
  538. 保存 GitHub OAuth 设置
  539. </Form.Button>
  540. <Divider />
  541. <Header as='h3'>
  542. 配置 WeChat Server
  543. <Header.Subheader>
  544. 用以支持通过微信进行登录注册,
  545. <a
  546. href='https://github.com/songquanpeng/wechat-server'
  547. target='_blank'
  548. >
  549. 点击此处
  550. </a>
  551. 了解 WeChat Server
  552. </Header.Subheader>
  553. </Header>
  554. <Form.Group widths={3}>
  555. <Form.Input
  556. label='WeChat Server 服务器地址'
  557. name='WeChatServerAddress'
  558. placeholder='例如:https://yourdomain.com'
  559. onChange={handleInputChange}
  560. autoComplete='new-password'
  561. value={inputs.WeChatServerAddress}
  562. />
  563. <Form.Input
  564. label='WeChat Server 访问凭证'
  565. name='WeChatServerToken'
  566. type='password'
  567. onChange={handleInputChange}
  568. autoComplete='new-password'
  569. value={inputs.WeChatServerToken}
  570. placeholder='敏感信息不会发送到前端显示'
  571. />
  572. <Form.Input
  573. label='微信公众号二维码图片链接'
  574. name='WeChatAccountQRCodeImageURL'
  575. onChange={handleInputChange}
  576. autoComplete='new-password'
  577. value={inputs.WeChatAccountQRCodeImageURL}
  578. placeholder='输入一个图片链接'
  579. />
  580. </Form.Group>
  581. <Form.Button onClick={submitWeChat}>
  582. 保存 WeChat Server 设置
  583. </Form.Button>
  584. <Divider />
  585. <Header as='h3'>配置 Telegram 登录</Header>
  586. <Form.Group inline>
  587. <Form.Input
  588. label='Telegram Bot Token'
  589. name='TelegramBotToken'
  590. value={inputs.TelegramBotToken}
  591. placeholder='输入你的 Telegram Bot Token'
  592. />
  593. <Form.Input
  594. label='Telegram Bot 名称'
  595. name='TelegramBotName'
  596. value={inputs.TelegramBotName}
  597. placeholder='输入你的 Telegram Bot 名称'
  598. />
  599. </Form.Group>
  600. <Form.Button onClick={submitTelegramSettings}>
  601. 保存 Telegram 登录设置
  602. </Form.Button>
  603. <Divider />
  604. <Header as='h3'>
  605. 配置 Turnstile
  606. <Header.Subheader>
  607. 用以支持用户校验,
  608. <a href='https://dash.cloudflare.com/' target='_blank'>
  609. 点击此处
  610. </a>
  611. 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
  612. </Header.Subheader>
  613. </Header>
  614. <Form.Group widths={3}>
  615. <Form.Input
  616. label='Turnstile Site Key'
  617. name='TurnstileSiteKey'
  618. onChange={handleInputChange}
  619. autoComplete='new-password'
  620. value={inputs.TurnstileSiteKey}
  621. placeholder='输入你注册的 Turnstile Site Key'
  622. />
  623. <Form.Input
  624. label='Turnstile Secret Key'
  625. name='TurnstileSecretKey'
  626. onChange={handleInputChange}
  627. type='password'
  628. autoComplete='new-password'
  629. value={inputs.TurnstileSecretKey}
  630. placeholder='敏感信息不会发送到前端显示'
  631. />
  632. </Form.Group>
  633. <Form.Button onClick={submitTurnstile}>
  634. 保存 Turnstile 设置
  635. </Form.Button>
  636. </Form>
  637. </Grid.Column>
  638. </Grid>
  639. );
  640. };
  641. export default SystemSetting;