SystemSetting.jsx 58 KB

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