SystemSetting.jsx 58 KB

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