SystemSetting.jsx 57 KB

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