SystemSetting.jsx 61 KB

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