SystemSetting.jsx 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState, useRef } from 'react';
  16. import {
  17. Button,
  18. Form,
  19. Row,
  20. Col,
  21. Typography,
  22. Modal,
  23. Banner,
  24. TagInput,
  25. Spin,
  26. Card,
  27. Radio,
  28. } from '@douyinfe/semi-ui';
  29. const { Text } = Typography;
  30. import {
  31. API,
  32. removeTrailingSlash,
  33. showError,
  34. showSuccess,
  35. toBoolean,
  36. } from '../../helpers';
  37. import axios from 'axios';
  38. import { useTranslation } from 'react-i18next';
  39. const SystemSetting = () => {
  40. const { t } = useTranslation();
  41. let [inputs, setInputs] = useState({
  42. PasswordLoginEnabled: '',
  43. PasswordRegisterEnabled: '',
  44. EmailVerificationEnabled: '',
  45. GitHubOAuthEnabled: '',
  46. GitHubClientId: '',
  47. GitHubClientSecret: '',
  48. 'oidc.enabled': '',
  49. 'oidc.client_id': '',
  50. 'oidc.client_secret': '',
  51. 'oidc.well_known': '',
  52. 'oidc.authorization_endpoint': '',
  53. 'oidc.token_endpoint': '',
  54. 'oidc.user_info_endpoint': '',
  55. Notice: '',
  56. SMTPServer: '',
  57. SMTPPort: '',
  58. SMTPAccount: '',
  59. SMTPFrom: '',
  60. SMTPToken: '',
  61. WorkerUrl: '',
  62. WorkerValidKey: '',
  63. WorkerAllowHttpImageRequestEnabled: '',
  64. Footer: '',
  65. WeChatAuthEnabled: '',
  66. WeChatServerAddress: '',
  67. WeChatServerToken: '',
  68. WeChatAccountQRCodeImageURL: '',
  69. TurnstileCheckEnabled: '',
  70. TurnstileSiteKey: '',
  71. TurnstileSecretKey: '',
  72. RegisterEnabled: '',
  73. EmailDomainRestrictionEnabled: '',
  74. EmailAliasRestrictionEnabled: '',
  75. SMTPSSLEnabled: '',
  76. EmailDomainWhitelist: [],
  77. TelegramOAuthEnabled: '',
  78. TelegramBotToken: '',
  79. TelegramBotName: '',
  80. LinuxDOOAuthEnabled: '',
  81. LinuxDOClientId: '',
  82. LinuxDOClientSecret: '',
  83. LinuxDOMinimumTrustLevel: '',
  84. ServerAddress: '',
  85. // SSRF防护配置
  86. 'fetch_setting.enable_ssrf_protection': true,
  87. 'fetch_setting.allow_private_ip': '',
  88. 'fetch_setting.domain_filter_mode': false, // true 白名单,false 黑名单
  89. 'fetch_setting.ip_filter_mode': false, // true 白名单,false 黑名单
  90. 'fetch_setting.domain_list': [],
  91. 'fetch_setting.ip_list': [],
  92. 'fetch_setting.allowed_ports': [],
  93. 'fetch_setting.apply_ip_filter_for_domain': false,
  94. });
  95. const [originInputs, setOriginInputs] = useState({});
  96. const [loading, setLoading] = useState(false);
  97. const [isLoaded, setIsLoaded] = useState(false);
  98. const formApiRef = useRef(null);
  99. const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
  100. const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] =
  101. useState(false);
  102. const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
  103. const [emailToAdd, setEmailToAdd] = useState('');
  104. const [domainFilterMode, setDomainFilterMode] = useState(true);
  105. const [ipFilterMode, setIpFilterMode] = useState(true);
  106. const [domainList, setDomainList] = useState([]);
  107. const [ipList, setIpList] = useState([]);
  108. const [allowedPorts, setAllowedPorts] = useState([]);
  109. const getOptions = async () => {
  110. setLoading(true);
  111. const res = await API.get('/api/option/');
  112. const { success, message, data } = res.data;
  113. if (success) {
  114. let newInputs = {};
  115. data.forEach((item) => {
  116. switch (item.key) {
  117. case 'TopupGroupRatio':
  118. item.value = JSON.stringify(JSON.parse(item.value), null, 2);
  119. break;
  120. case 'EmailDomainWhitelist':
  121. setEmailDomainWhitelist(item.value ? item.value.split(',') : []);
  122. break;
  123. case 'fetch_setting.allow_private_ip':
  124. case 'fetch_setting.enable_ssrf_protection':
  125. case 'fetch_setting.domain_filter_mode':
  126. case 'fetch_setting.ip_filter_mode':
  127. case 'fetch_setting.apply_ip_filter_for_domain':
  128. item.value = toBoolean(item.value);
  129. break;
  130. case 'fetch_setting.domain_list':
  131. try {
  132. const domains = item.value ? JSON.parse(item.value) : [];
  133. setDomainList(Array.isArray(domains) ? domains : []);
  134. } catch (e) {
  135. setDomainList([]);
  136. }
  137. break;
  138. case 'fetch_setting.ip_list':
  139. try {
  140. const ips = item.value ? JSON.parse(item.value) : [];
  141. setIpList(Array.isArray(ips) ? ips : []);
  142. } catch (e) {
  143. setIpList([]);
  144. }
  145. break;
  146. case 'fetch_setting.allowed_ports':
  147. try {
  148. const ports = item.value ? JSON.parse(item.value) : [];
  149. setAllowedPorts(Array.isArray(ports) ? ports : []);
  150. } catch (e) {
  151. setAllowedPorts(['80', '443', '8080', '8443']);
  152. }
  153. break;
  154. case 'PasswordLoginEnabled':
  155. case 'PasswordRegisterEnabled':
  156. case 'EmailVerificationEnabled':
  157. case 'GitHubOAuthEnabled':
  158. case 'WeChatAuthEnabled':
  159. case 'TelegramOAuthEnabled':
  160. case 'RegisterEnabled':
  161. case 'TurnstileCheckEnabled':
  162. case 'EmailDomainRestrictionEnabled':
  163. case 'EmailAliasRestrictionEnabled':
  164. case 'SMTPSSLEnabled':
  165. case 'LinuxDOOAuthEnabled':
  166. case 'oidc.enabled':
  167. case 'WorkerAllowHttpImageRequestEnabled':
  168. item.value = toBoolean(item.value);
  169. break;
  170. case 'Price':
  171. case 'MinTopUp':
  172. item.value = parseFloat(item.value);
  173. break;
  174. default:
  175. break;
  176. }
  177. newInputs[item.key] = item.value;
  178. });
  179. setInputs(newInputs);
  180. setOriginInputs(newInputs);
  181. // 同步模式布尔到本地状态
  182. if (
  183. typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined'
  184. ) {
  185. setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']);
  186. }
  187. if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') {
  188. setIpFilterMode(!!newInputs['fetch_setting.ip_filter_mode']);
  189. }
  190. if (formApiRef.current) {
  191. formApiRef.current.setValues(newInputs);
  192. }
  193. setIsLoaded(true);
  194. } else {
  195. showError(message);
  196. }
  197. setLoading(false);
  198. };
  199. useEffect(() => {
  200. getOptions();
  201. }, []);
  202. const updateOptions = async (options) => {
  203. setLoading(true);
  204. try {
  205. // 分离 checkbox 类型的选项和其他选项
  206. const checkboxOptions = options.filter((opt) =>
  207. opt.key.toLowerCase().endsWith('enabled'),
  208. );
  209. const otherOptions = options.filter(
  210. (opt) => !opt.key.toLowerCase().endsWith('enabled'),
  211. );
  212. // 处理 checkbox 类型的选项
  213. for (const opt of checkboxOptions) {
  214. const res = await API.put('/api/option/', {
  215. key: opt.key,
  216. value: opt.value.toString(),
  217. });
  218. if (!res.data.success) {
  219. showError(res.data.message);
  220. return;
  221. }
  222. }
  223. // 处理其他选项
  224. if (otherOptions.length > 0) {
  225. const requestQueue = otherOptions.map((opt) =>
  226. API.put('/api/option/', {
  227. key: opt.key,
  228. value:
  229. typeof opt.value === 'boolean' ? opt.value.toString() : opt.value,
  230. }),
  231. );
  232. const results = await Promise.all(requestQueue);
  233. // 检查所有请求是否成功
  234. const errorResults = results.filter((res) => !res.data.success);
  235. errorResults.forEach((res) => {
  236. showError(res.data.message);
  237. });
  238. }
  239. showSuccess(t('更新成功'));
  240. // 更新本地状态
  241. const newInputs = { ...inputs };
  242. options.forEach((opt) => {
  243. newInputs[opt.key] = opt.value;
  244. });
  245. setInputs(newInputs);
  246. } catch (error) {
  247. showError(t('更新失败'));
  248. }
  249. setLoading(false);
  250. };
  251. const handleFormChange = (values) => {
  252. setInputs(values);
  253. };
  254. const submitWorker = async () => {
  255. let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
  256. const options = [
  257. { key: 'WorkerUrl', value: WorkerUrl },
  258. {
  259. key: 'WorkerAllowHttpImageRequestEnabled',
  260. value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false',
  261. },
  262. ];
  263. if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
  264. options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
  265. }
  266. await updateOptions(options);
  267. };
  268. const submitServerAddress = async () => {
  269. let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
  270. await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
  271. };
  272. const submitSMTP = async () => {
  273. const options = [];
  274. if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
  275. options.push({ key: 'SMTPServer', value: inputs.SMTPServer });
  276. }
  277. if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
  278. options.push({ key: 'SMTPAccount', value: inputs.SMTPAccount });
  279. }
  280. if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
  281. options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom });
  282. }
  283. if (
  284. originInputs['SMTPPort'] !== inputs.SMTPPort &&
  285. inputs.SMTPPort !== ''
  286. ) {
  287. options.push({ key: 'SMTPPort', value: inputs.SMTPPort });
  288. }
  289. if (
  290. originInputs['SMTPToken'] !== inputs.SMTPToken &&
  291. inputs.SMTPToken !== ''
  292. ) {
  293. options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
  294. }
  295. if (options.length > 0) {
  296. await updateOptions(options);
  297. }
  298. };
  299. const submitEmailDomainWhitelist = async () => {
  300. if (Array.isArray(emailDomainWhitelist)) {
  301. await updateOptions([
  302. {
  303. key: 'EmailDomainWhitelist',
  304. value: emailDomainWhitelist.join(','),
  305. },
  306. ]);
  307. } else {
  308. showError(t('邮箱域名白名单格式不正确'));
  309. }
  310. };
  311. const submitSSRF = async () => {
  312. const options = [];
  313. // 处理域名过滤模式与列表
  314. options.push({
  315. key: 'fetch_setting.domain_filter_mode',
  316. value: domainFilterMode,
  317. });
  318. if (Array.isArray(domainList)) {
  319. options.push({
  320. key: 'fetch_setting.domain_list',
  321. value: JSON.stringify(domainList),
  322. });
  323. }
  324. // 处理IP过滤模式与列表
  325. options.push({
  326. key: 'fetch_setting.ip_filter_mode',
  327. value: ipFilterMode,
  328. });
  329. if (Array.isArray(ipList)) {
  330. options.push({
  331. key: 'fetch_setting.ip_list',
  332. value: JSON.stringify(ipList),
  333. });
  334. }
  335. // 处理端口配置
  336. if (Array.isArray(allowedPorts)) {
  337. options.push({
  338. key: 'fetch_setting.allowed_ports',
  339. value: JSON.stringify(allowedPorts),
  340. });
  341. }
  342. if (options.length > 0) {
  343. await updateOptions(options);
  344. }
  345. };
  346. const handleAddEmail = () => {
  347. if (emailToAdd && emailToAdd.trim() !== '') {
  348. const domain = emailToAdd.trim();
  349. // 验证域名格式
  350. const domainRegex =
  351. /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
  352. if (!domainRegex.test(domain)) {
  353. showError(t('邮箱域名格式不正确,请输入有效的域名,如 gmail.com'));
  354. return;
  355. }
  356. // 检查是否已存在
  357. if (emailDomainWhitelist.includes(domain)) {
  358. showError(t('该域名已存在于白名单中'));
  359. return;
  360. }
  361. setEmailDomainWhitelist([...emailDomainWhitelist, domain]);
  362. setEmailToAdd('');
  363. showSuccess(t('已添加到白名单'));
  364. }
  365. };
  366. const submitWeChat = async () => {
  367. const options = [];
  368. if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
  369. options.push({
  370. key: 'WeChatServerAddress',
  371. value: removeTrailingSlash(inputs.WeChatServerAddress),
  372. });
  373. }
  374. if (
  375. originInputs['WeChatAccountQRCodeImageURL'] !==
  376. inputs.WeChatAccountQRCodeImageURL
  377. ) {
  378. options.push({
  379. key: 'WeChatAccountQRCodeImageURL',
  380. value: inputs.WeChatAccountQRCodeImageURL,
  381. });
  382. }
  383. if (
  384. originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
  385. inputs.WeChatServerToken !== ''
  386. ) {
  387. options.push({
  388. key: 'WeChatServerToken',
  389. value: inputs.WeChatServerToken,
  390. });
  391. }
  392. if (options.length > 0) {
  393. await updateOptions(options);
  394. }
  395. };
  396. const submitGitHubOAuth = async () => {
  397. const options = [];
  398. if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
  399. options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId });
  400. }
  401. if (
  402. originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
  403. inputs.GitHubClientSecret !== ''
  404. ) {
  405. options.push({
  406. key: 'GitHubClientSecret',
  407. value: inputs.GitHubClientSecret,
  408. });
  409. }
  410. if (options.length > 0) {
  411. await updateOptions(options);
  412. }
  413. };
  414. const submitOIDCSettings = async () => {
  415. if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') {
  416. if (
  417. !inputs['oidc.well_known'].startsWith('http://') &&
  418. !inputs['oidc.well_known'].startsWith('https://')
  419. ) {
  420. showError(t('Well-Known URL 必须以 http:// 或 https:// 开头'));
  421. return;
  422. }
  423. try {
  424. const res = await axios.create().get(inputs['oidc.well_known']);
  425. inputs['oidc.authorization_endpoint'] =
  426. res.data['authorization_endpoint'];
  427. inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
  428. inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
  429. showSuccess(t('获取 OIDC 配置成功!'));
  430. } catch (err) {
  431. console.error(err);
  432. showError(
  433. t('获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确'),
  434. );
  435. return;
  436. }
  437. }
  438. const options = [];
  439. if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
  440. options.push({
  441. key: 'oidc.well_known',
  442. value: inputs['oidc.well_known'],
  443. });
  444. }
  445. if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
  446. options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] });
  447. }
  448. if (
  449. originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] &&
  450. inputs['oidc.client_secret'] !== ''
  451. ) {
  452. options.push({
  453. key: 'oidc.client_secret',
  454. value: inputs['oidc.client_secret'],
  455. });
  456. }
  457. if (
  458. originInputs['oidc.authorization_endpoint'] !==
  459. inputs['oidc.authorization_endpoint']
  460. ) {
  461. options.push({
  462. key: 'oidc.authorization_endpoint',
  463. value: inputs['oidc.authorization_endpoint'],
  464. });
  465. }
  466. if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
  467. options.push({
  468. key: 'oidc.token_endpoint',
  469. value: inputs['oidc.token_endpoint'],
  470. });
  471. }
  472. if (
  473. originInputs['oidc.user_info_endpoint'] !==
  474. inputs['oidc.user_info_endpoint']
  475. ) {
  476. options.push({
  477. key: 'oidc.user_info_endpoint',
  478. value: inputs['oidc.user_info_endpoint'],
  479. });
  480. }
  481. if (options.length > 0) {
  482. await updateOptions(options);
  483. }
  484. };
  485. const submitTelegramSettings = async () => {
  486. const options = [
  487. { key: 'TelegramBotToken', value: inputs.TelegramBotToken },
  488. { key: 'TelegramBotName', value: inputs.TelegramBotName },
  489. ];
  490. await updateOptions(options);
  491. };
  492. const submitTurnstile = async () => {
  493. const options = [];
  494. if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
  495. options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey });
  496. }
  497. if (
  498. originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
  499. inputs.TurnstileSecretKey !== ''
  500. ) {
  501. options.push({
  502. key: 'TurnstileSecretKey',
  503. value: inputs.TurnstileSecretKey,
  504. });
  505. }
  506. if (options.length > 0) {
  507. await updateOptions(options);
  508. }
  509. };
  510. const submitLinuxDOOAuth = async () => {
  511. const options = [];
  512. if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
  513. options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId });
  514. }
  515. if (
  516. originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret &&
  517. inputs.LinuxDOClientSecret !== ''
  518. ) {
  519. options.push({
  520. key: 'LinuxDOClientSecret',
  521. value: inputs.LinuxDOClientSecret,
  522. });
  523. }
  524. if (
  525. originInputs['LinuxDOMinimumTrustLevel'] !==
  526. inputs.LinuxDOMinimumTrustLevel
  527. ) {
  528. options.push({
  529. key: 'LinuxDOMinimumTrustLevel',
  530. value: inputs.LinuxDOMinimumTrustLevel,
  531. });
  532. }
  533. if (options.length > 0) {
  534. await updateOptions(options);
  535. }
  536. };
  537. const handleCheckboxChange = async (optionKey, event) => {
  538. const value = event.target.checked;
  539. if (optionKey === 'PasswordLoginEnabled' && !value) {
  540. setShowPasswordLoginConfirmModal(true);
  541. } else {
  542. await updateOptions([{ key: optionKey, value }]);
  543. }
  544. if (optionKey === 'LinuxDOOAuthEnabled') {
  545. setLinuxDOOAuthEnabled(value);
  546. }
  547. };
  548. const handlePasswordLoginConfirm = async () => {
  549. await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]);
  550. setShowPasswordLoginConfirmModal(false);
  551. };
  552. return (
  553. <div>
  554. {isLoaded ? (
  555. <Form
  556. initValues={inputs}
  557. onValueChange={handleFormChange}
  558. getFormApi={(api) => (formApiRef.current = api)}
  559. >
  560. {({ formState, values, formApi }) => (
  561. <div
  562. style={{
  563. display: 'flex',
  564. flexDirection: 'column',
  565. gap: '10px',
  566. marginTop: '10px',
  567. }}
  568. >
  569. <Card>
  570. <Form.Section text={t('通用设置')}>
  571. <Row
  572. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  573. >
  574. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  575. <Form.Input
  576. field='ServerAddress'
  577. label={t('服务器地址')}
  578. placeholder='https://yourdomain.com'
  579. extraText={t(
  580. '该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
  581. )}
  582. />
  583. </Col>
  584. </Row>
  585. <Button onClick={submitServerAddress}>
  586. {t('更新服务器地址')}
  587. </Button>
  588. </Form.Section>
  589. </Card>
  590. <Card>
  591. <Form.Section text={t('代理设置')}>
  592. <Text>
  593. (支持{' '}
  594. <a
  595. href='https://github.com/Calcium-Ion/new-api-worker'
  596. target='_blank'
  597. rel='noreferrer'
  598. >
  599. new-api-worker
  600. </a>
  601. </Text>
  602. <Row
  603. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  604. >
  605. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  606. <Form.Input
  607. field='WorkerUrl'
  608. label={t('Worker地址')}
  609. placeholder='例如:https://workername.yourdomain.workers.dev'
  610. />
  611. </Col>
  612. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  613. <Form.Input
  614. field='WorkerValidKey'
  615. label={t('Worker密钥')}
  616. placeholder='敏感信息不会发送到前端显示'
  617. type='password'
  618. />
  619. </Col>
  620. </Row>
  621. <Form.Checkbox
  622. field='WorkerAllowHttpImageRequestEnabled'
  623. noLabel
  624. >
  625. {t('允许 HTTP 协议图片请求(适用于自部署代理)')}
  626. </Form.Checkbox>
  627. <Button onClick={submitWorker}>{t('更新Worker设置')}</Button>
  628. </Form.Section>
  629. </Card>
  630. <Card>
  631. <Form.Section text={t('SSRF防护设置')}>
  632. <Text extraText={t('SSRF防护详细说明')}>
  633. {t('配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全')}
  634. </Text>
  635. <Row
  636. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  637. >
  638. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  639. <Form.Checkbox
  640. field='fetch_setting.enable_ssrf_protection'
  641. noLabel
  642. extraText={t('SSRF防护开关详细说明')}
  643. onChange={(e) =>
  644. handleCheckboxChange(
  645. 'fetch_setting.enable_ssrf_protection',
  646. e,
  647. )
  648. }
  649. >
  650. {t('启用SSRF防护(推荐开启以保护服务器安全)')}
  651. </Form.Checkbox>
  652. </Col>
  653. </Row>
  654. <Row
  655. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  656. style={{ marginTop: 16 }}
  657. >
  658. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  659. <Form.Checkbox
  660. field='fetch_setting.allow_private_ip'
  661. noLabel
  662. extraText={t('私有IP访问详细说明')}
  663. onChange={(e) =>
  664. handleCheckboxChange(
  665. 'fetch_setting.allow_private_ip',
  666. e,
  667. )
  668. }
  669. >
  670. {t(
  671. '允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)',
  672. )}
  673. </Form.Checkbox>
  674. </Col>
  675. </Row>
  676. <Row
  677. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  678. style={{ marginTop: 16 }}
  679. >
  680. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  681. <Form.Checkbox
  682. field='fetch_setting.apply_ip_filter_for_domain'
  683. noLabel
  684. extraText={t('域名IP过滤详细说明')}
  685. onChange={(e) =>
  686. handleCheckboxChange(
  687. 'fetch_setting.apply_ip_filter_for_domain',
  688. e,
  689. )
  690. }
  691. style={{ marginBottom: 8 }}
  692. >
  693. {t('对域名启用 IP 过滤(实验性)')}
  694. </Form.Checkbox>
  695. <Text strong>
  696. {t(domainFilterMode ? '域名白名单' : '域名黑名单')}
  697. </Text>
  698. <Text
  699. type='secondary'
  700. style={{ display: 'block', marginBottom: 8 }}
  701. >
  702. {t(
  703. '支持通配符格式,如:example.com, *.api.example.com',
  704. )}
  705. </Text>
  706. <Radio.Group
  707. type='button'
  708. value={domainFilterMode ? 'whitelist' : 'blacklist'}
  709. onChange={(val) => {
  710. const selected =
  711. val && val.target ? val.target.value : val;
  712. const isWhitelist = selected === 'whitelist';
  713. setDomainFilterMode(isWhitelist);
  714. setInputs((prev) => ({
  715. ...prev,
  716. 'fetch_setting.domain_filter_mode': isWhitelist,
  717. }));
  718. }}
  719. style={{ marginBottom: 8 }}
  720. >
  721. <Radio value='whitelist'>{t('白名单')}</Radio>
  722. <Radio value='blacklist'>{t('黑名单')}</Radio>
  723. </Radio.Group>
  724. <TagInput
  725. value={domainList}
  726. onChange={(value) => {
  727. setDomainList(value);
  728. // 触发Form的onChange事件
  729. setInputs((prev) => ({
  730. ...prev,
  731. 'fetch_setting.domain_list': value,
  732. }));
  733. }}
  734. placeholder={t('输入域名后回车,如:example.com')}
  735. style={{ width: '100%' }}
  736. />
  737. </Col>
  738. </Row>
  739. <Row
  740. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  741. style={{ marginTop: 16 }}
  742. >
  743. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  744. <Text strong>
  745. {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')}
  746. </Text>
  747. <Text
  748. type='secondary'
  749. style={{ display: 'block', marginBottom: 8 }}
  750. >
  751. {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')}
  752. </Text>
  753. <Radio.Group
  754. type='button'
  755. value={ipFilterMode ? 'whitelist' : 'blacklist'}
  756. onChange={(val) => {
  757. const selected =
  758. val && val.target ? val.target.value : val;
  759. const isWhitelist = selected === 'whitelist';
  760. setIpFilterMode(isWhitelist);
  761. setInputs((prev) => ({
  762. ...prev,
  763. 'fetch_setting.ip_filter_mode': isWhitelist,
  764. }));
  765. }}
  766. style={{ marginBottom: 8 }}
  767. >
  768. <Radio value='whitelist'>{t('白名单')}</Radio>
  769. <Radio value='blacklist'>{t('黑名单')}</Radio>
  770. </Radio.Group>
  771. <TagInput
  772. value={ipList}
  773. onChange={(value) => {
  774. setIpList(value);
  775. // 触发Form的onChange事件
  776. setInputs((prev) => ({
  777. ...prev,
  778. 'fetch_setting.ip_list': value,
  779. }));
  780. }}
  781. placeholder={t('输入IP地址后回车,如:8.8.8.8')}
  782. style={{ width: '100%' }}
  783. />
  784. </Col>
  785. </Row>
  786. <Row
  787. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  788. style={{ marginTop: 16 }}
  789. >
  790. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  791. <Text strong>{t('允许的端口')}</Text>
  792. <Text
  793. type='secondary'
  794. style={{ display: 'block', marginBottom: 8 }}
  795. >
  796. {t('支持单个端口和端口范围,如:80, 443, 8000-8999')}
  797. </Text>
  798. <TagInput
  799. value={allowedPorts}
  800. onChange={(value) => {
  801. setAllowedPorts(value);
  802. // 触发Form的onChange事件
  803. setInputs((prev) => ({
  804. ...prev,
  805. 'fetch_setting.allowed_ports': value,
  806. }));
  807. }}
  808. placeholder={t('输入端口后回车,如:80 或 8000-8999')}
  809. style={{ width: '100%' }}
  810. />
  811. <Text
  812. type='secondary'
  813. style={{ display: 'block', marginBottom: 8 }}
  814. >
  815. {t('端口配置详细说明')}
  816. </Text>
  817. </Col>
  818. </Row>
  819. <Button onClick={submitSSRF} style={{ marginTop: 16 }}>
  820. {t('更新SSRF防护设置')}
  821. </Button>
  822. </Form.Section>
  823. </Card>
  824. <Card>
  825. <Form.Section text={t('配置登录注册')}>
  826. <Row
  827. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  828. >
  829. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  830. <Form.Checkbox
  831. field='PasswordLoginEnabled'
  832. noLabel
  833. onChange={(e) =>
  834. handleCheckboxChange('PasswordLoginEnabled', e)
  835. }
  836. >
  837. {t('允许通过密码进行登录')}
  838. </Form.Checkbox>
  839. <Form.Checkbox
  840. field='PasswordRegisterEnabled'
  841. noLabel
  842. onChange={(e) =>
  843. handleCheckboxChange('PasswordRegisterEnabled', e)
  844. }
  845. >
  846. {t('允许通过密码进行注册')}
  847. </Form.Checkbox>
  848. <Form.Checkbox
  849. field='EmailVerificationEnabled'
  850. noLabel
  851. onChange={(e) =>
  852. handleCheckboxChange('EmailVerificationEnabled', e)
  853. }
  854. >
  855. {t('通过密码注册时需要进行邮箱验证')}
  856. </Form.Checkbox>
  857. <Form.Checkbox
  858. field='RegisterEnabled'
  859. noLabel
  860. onChange={(e) =>
  861. handleCheckboxChange('RegisterEnabled', e)
  862. }
  863. >
  864. {t('允许新用户注册')}
  865. </Form.Checkbox>
  866. <Form.Checkbox
  867. field='TurnstileCheckEnabled'
  868. noLabel
  869. onChange={(e) =>
  870. handleCheckboxChange('TurnstileCheckEnabled', e)
  871. }
  872. >
  873. {t('允许 Turnstile 用户校验')}
  874. </Form.Checkbox>
  875. </Col>
  876. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  877. <Form.Checkbox
  878. field='GitHubOAuthEnabled'
  879. noLabel
  880. onChange={(e) =>
  881. handleCheckboxChange('GitHubOAuthEnabled', e)
  882. }
  883. >
  884. {t('允许通过 GitHub 账户登录 & 注册')}
  885. </Form.Checkbox>
  886. <Form.Checkbox
  887. field='LinuxDOOAuthEnabled'
  888. noLabel
  889. onChange={(e) =>
  890. handleCheckboxChange('LinuxDOOAuthEnabled', e)
  891. }
  892. >
  893. {t('允许通过 Linux DO 账户登录 & 注册')}
  894. </Form.Checkbox>
  895. <Form.Checkbox
  896. field='WeChatAuthEnabled'
  897. noLabel
  898. onChange={(e) =>
  899. handleCheckboxChange('WeChatAuthEnabled', e)
  900. }
  901. >
  902. {t('允许通过微信登录 & 注册')}
  903. </Form.Checkbox>
  904. <Form.Checkbox
  905. field='TelegramOAuthEnabled'
  906. noLabel
  907. onChange={(e) =>
  908. handleCheckboxChange('TelegramOAuthEnabled', e)
  909. }
  910. >
  911. {t('允许通过 Telegram 进行登录')}
  912. </Form.Checkbox>
  913. <Form.Checkbox
  914. field="['oidc.enabled']"
  915. noLabel
  916. onChange={(e) =>
  917. handleCheckboxChange('oidc.enabled', e)
  918. }
  919. >
  920. {t('允许通过 OIDC 进行登录')}
  921. </Form.Checkbox>
  922. </Col>
  923. </Row>
  924. </Form.Section>
  925. </Card>
  926. <Card>
  927. <Form.Section text={t('配置邮箱域名白名单')}>
  928. <Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text>
  929. <Row
  930. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  931. >
  932. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  933. <Form.Checkbox
  934. field='EmailDomainRestrictionEnabled'
  935. noLabel
  936. onChange={(e) =>
  937. handleCheckboxChange(
  938. 'EmailDomainRestrictionEnabled',
  939. e,
  940. )
  941. }
  942. >
  943. 启用邮箱域名白名单
  944. </Form.Checkbox>
  945. </Col>
  946. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  947. <Form.Checkbox
  948. field='EmailAliasRestrictionEnabled'
  949. noLabel
  950. onChange={(e) =>
  951. handleCheckboxChange(
  952. 'EmailAliasRestrictionEnabled',
  953. e,
  954. )
  955. }
  956. >
  957. 启用邮箱别名限制
  958. </Form.Checkbox>
  959. </Col>
  960. </Row>
  961. <TagInput
  962. value={emailDomainWhitelist}
  963. onChange={setEmailDomainWhitelist}
  964. placeholder={t('输入域名后回车')}
  965. style={{ width: '100%', marginTop: 16 }}
  966. />
  967. <Form.Input
  968. placeholder={t('输入要添加的邮箱域名')}
  969. value={emailToAdd}
  970. onChange={(value) => setEmailToAdd(value)}
  971. style={{ marginTop: 16 }}
  972. suffix={
  973. <Button
  974. theme='solid'
  975. type='primary'
  976. onClick={handleAddEmail}
  977. >
  978. {t('添加')}
  979. </Button>
  980. }
  981. onEnterPress={handleAddEmail}
  982. />
  983. <Button
  984. onClick={submitEmailDomainWhitelist}
  985. style={{ marginTop: 10 }}
  986. >
  987. {t('保存邮箱域名白名单设置')}
  988. </Button>
  989. </Form.Section>
  990. </Card>
  991. <Card>
  992. <Form.Section text={t('配置 SMTP')}>
  993. <Text>{t('用以支持系统的邮件发送')}</Text>
  994. <Row
  995. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  996. >
  997. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  998. <Form.Input
  999. field='SMTPServer'
  1000. label={t('SMTP 服务器地址')}
  1001. />
  1002. </Col>
  1003. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1004. <Form.Input field='SMTPPort' label={t('SMTP 端口')} />
  1005. </Col>
  1006. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1007. <Form.Input field='SMTPAccount' label={t('SMTP 账户')} />
  1008. </Col>
  1009. </Row>
  1010. <Row
  1011. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1012. style={{ marginTop: 16 }}
  1013. >
  1014. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1015. <Form.Input
  1016. field='SMTPFrom'
  1017. label={t('SMTP 发送者邮箱')}
  1018. />
  1019. </Col>
  1020. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1021. <Form.Input
  1022. field='SMTPToken'
  1023. label={t('SMTP 访问凭证')}
  1024. type='password'
  1025. placeholder='敏感信息不会发送到前端显示'
  1026. />
  1027. </Col>
  1028. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1029. <Form.Checkbox
  1030. field='SMTPSSLEnabled'
  1031. noLabel
  1032. onChange={(e) =>
  1033. handleCheckboxChange('SMTPSSLEnabled', e)
  1034. }
  1035. >
  1036. {t('启用SMTP SSL')}
  1037. </Form.Checkbox>
  1038. </Col>
  1039. </Row>
  1040. <Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button>
  1041. </Form.Section>
  1042. </Card>
  1043. <Card>
  1044. <Form.Section text={t('配置 OIDC')}>
  1045. <Text>
  1046. {t(
  1047. '用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP',
  1048. )}
  1049. </Text>
  1050. <Banner
  1051. type='info'
  1052. description={`${t('主页链接填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('重定向 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/oidc`}
  1053. style={{ marginBottom: 20, marginTop: 16 }}
  1054. />
  1055. <Text>
  1056. {t(
  1057. '若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置',
  1058. )}
  1059. </Text>
  1060. <Row
  1061. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1062. >
  1063. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1064. <Form.Input
  1065. field="['oidc.well_known']"
  1066. label={t('Well-Known URL')}
  1067. placeholder={t('请输入 OIDC 的 Well-Known URL')}
  1068. />
  1069. </Col>
  1070. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1071. <Form.Input
  1072. field="['oidc.client_id']"
  1073. label={t('Client ID')}
  1074. placeholder={t('输入 OIDC 的 Client ID')}
  1075. />
  1076. </Col>
  1077. </Row>
  1078. <Row
  1079. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1080. >
  1081. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1082. <Form.Input
  1083. field="['oidc.client_secret']"
  1084. label={t('Client Secret')}
  1085. type='password'
  1086. placeholder={t('敏感信息不会发送到前端显示')}
  1087. />
  1088. </Col>
  1089. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1090. <Form.Input
  1091. field="['oidc.authorization_endpoint']"
  1092. label={t('Authorization Endpoint')}
  1093. placeholder={t('输入 OIDC 的 Authorization Endpoint')}
  1094. />
  1095. </Col>
  1096. </Row>
  1097. <Row
  1098. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1099. >
  1100. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1101. <Form.Input
  1102. field="['oidc.token_endpoint']"
  1103. label={t('Token Endpoint')}
  1104. placeholder={t('输入 OIDC 的 Token Endpoint')}
  1105. />
  1106. </Col>
  1107. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1108. <Form.Input
  1109. field="['oidc.user_info_endpoint']"
  1110. label={t('User Info Endpoint')}
  1111. placeholder={t('输入 OIDC 的 Userinfo Endpoint')}
  1112. />
  1113. </Col>
  1114. </Row>
  1115. <Button onClick={submitOIDCSettings}>
  1116. {t('保存 OIDC 设置')}
  1117. </Button>
  1118. </Form.Section>
  1119. </Card>
  1120. <Card>
  1121. <Form.Section text={t('配置 GitHub OAuth App')}>
  1122. <Text>{t('用以支持通过 GitHub 进行登录注册')}</Text>
  1123. <Banner
  1124. type='info'
  1125. description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/github`}
  1126. style={{ marginBottom: 20, marginTop: 16 }}
  1127. />
  1128. <Row
  1129. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1130. >
  1131. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1132. <Form.Input
  1133. field='GitHubClientId'
  1134. label={t('GitHub Client ID')}
  1135. />
  1136. </Col>
  1137. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1138. <Form.Input
  1139. field='GitHubClientSecret'
  1140. label={t('GitHub Client Secret')}
  1141. type='password'
  1142. placeholder={t('敏感信息不会发送到前端显示')}
  1143. />
  1144. </Col>
  1145. </Row>
  1146. <Button onClick={submitGitHubOAuth}>
  1147. {t('保存 GitHub OAuth 设置')}
  1148. </Button>
  1149. </Form.Section>
  1150. </Card>
  1151. <Card>
  1152. <Form.Section text={t('配置 Linux DO OAuth')}>
  1153. <Text>
  1154. {t('用以支持通过 Linux DO 进行登录注册')}
  1155. <a
  1156. href='https://connect.linux.do/'
  1157. target='_blank'
  1158. rel='noreferrer'
  1159. style={{
  1160. display: 'inline-block',
  1161. marginLeft: 4,
  1162. marginRight: 4,
  1163. }}
  1164. >
  1165. {t('点击此处')}
  1166. </a>
  1167. {t('管理你的 LinuxDO OAuth App')}
  1168. </Text>
  1169. <Banner
  1170. type='info'
  1171. description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/linuxdo`}
  1172. style={{ marginBottom: 20, marginTop: 16 }}
  1173. />
  1174. <Row
  1175. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1176. >
  1177. <Col xs={24} sm={24} md={10} lg={10} xl={10}>
  1178. <Form.Input
  1179. field='LinuxDOClientId'
  1180. label={t('Linux DO Client ID')}
  1181. placeholder={t('输入你注册的 LinuxDO OAuth APP 的 ID')}
  1182. />
  1183. </Col>
  1184. <Col xs={24} sm={24} md={10} lg={10} xl={10}>
  1185. <Form.Input
  1186. field='LinuxDOClientSecret'
  1187. label={t('Linux DO Client Secret')}
  1188. type='password'
  1189. placeholder={t('敏感信息不会发送到前端显示')}
  1190. />
  1191. </Col>
  1192. <Col xs={24} sm={24} md={4} lg={4} xl={4}>
  1193. <Form.Input
  1194. field='LinuxDOMinimumTrustLevel'
  1195. label='LinuxDO Minimum Trust Level'
  1196. placeholder='允许注册的最低信任等级'
  1197. />
  1198. </Col>
  1199. </Row>
  1200. <Button onClick={submitLinuxDOOAuth}>
  1201. {t('保存 Linux DO OAuth 设置')}
  1202. </Button>
  1203. </Form.Section>
  1204. </Card>
  1205. <Card>
  1206. <Form.Section text={t('配置 WeChat Server')}>
  1207. <Text>{t('用以支持通过微信进行登录注册')}</Text>
  1208. <Row
  1209. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1210. >
  1211. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1212. <Form.Input
  1213. field='WeChatServerAddress'
  1214. label={t('WeChat Server 服务器地址')}
  1215. />
  1216. </Col>
  1217. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1218. <Form.Input
  1219. field='WeChatServerToken'
  1220. label={t('WeChat Server 访问凭证')}
  1221. type='password'
  1222. placeholder={t('敏感信息不会发送到前端显示')}
  1223. />
  1224. </Col>
  1225. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1226. <Form.Input
  1227. field='WeChatAccountQRCodeImageURL'
  1228. label={t('微信公众号二维码图片链接')}
  1229. />
  1230. </Col>
  1231. </Row>
  1232. <Button onClick={submitWeChat}>
  1233. {t('保存 WeChat Server 设置')}
  1234. </Button>
  1235. </Form.Section>
  1236. </Card>
  1237. <Card>
  1238. <Form.Section text={t('配置 Telegram 登录')}>
  1239. <Text>{t('用以支持通过 Telegram 进行登录注册')}</Text>
  1240. <Row
  1241. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1242. >
  1243. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1244. <Form.Input
  1245. field='TelegramBotToken'
  1246. label={t('Telegram Bot Token')}
  1247. placeholder={t('敏感信息不会发送到前端显示')}
  1248. type='password'
  1249. />
  1250. </Col>
  1251. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1252. <Form.Input
  1253. field='TelegramBotName'
  1254. label={t('Telegram Bot 名称')}
  1255. />
  1256. </Col>
  1257. </Row>
  1258. <Button onClick={submitTelegramSettings}>
  1259. {t('保存 Telegram 登录设置')}
  1260. </Button>
  1261. </Form.Section>
  1262. </Card>
  1263. <Card>
  1264. <Form.Section text={t('配置 Turnstile')}>
  1265. <Text>{t('用以支持用户校验')}</Text>
  1266. <Row
  1267. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1268. >
  1269. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1270. <Form.Input
  1271. field='TurnstileSiteKey'
  1272. label={t('Turnstile Site Key')}
  1273. />
  1274. </Col>
  1275. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1276. <Form.Input
  1277. field='TurnstileSecretKey'
  1278. label={t('Turnstile Secret Key')}
  1279. type='password'
  1280. placeholder={t('敏感信息不会发送到前端显示')}
  1281. />
  1282. </Col>
  1283. </Row>
  1284. <Button onClick={submitTurnstile}>
  1285. {t('保存 Turnstile 设置')}
  1286. </Button>
  1287. </Form.Section>
  1288. </Card>
  1289. <Modal
  1290. title={t('确认取消密码登录')}
  1291. visible={showPasswordLoginConfirmModal}
  1292. onOk={handlePasswordLoginConfirm}
  1293. onCancel={() => {
  1294. setShowPasswordLoginConfirmModal(false);
  1295. formApiRef.current.setValue('PasswordLoginEnabled', true);
  1296. }}
  1297. okText={t('确认')}
  1298. cancelText={t('取消')}
  1299. >
  1300. <p>
  1301. {t(
  1302. '您确定要取消密码登录功能吗?这可能会影响用户的登录方式。',
  1303. )}
  1304. </p>
  1305. </Modal>
  1306. </div>
  1307. )}
  1308. </Form>
  1309. ) : (
  1310. <div
  1311. style={{
  1312. display: 'flex',
  1313. justifyContent: 'center',
  1314. alignItems: 'center',
  1315. height: '100vh',
  1316. }}
  1317. >
  1318. <Spin size='large' />
  1319. </div>
  1320. )}
  1321. </div>
  1322. );
  1323. };
  1324. export default SystemSetting;