SystemSetting.jsx 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584
  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 [email protected]
  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:
  568. formValues['passkey.rp_display_name'] ||
  569. inputs['passkey.rp_display_name'] ||
  570. '',
  571. });
  572. options.push({
  573. key: 'passkey.rp_id',
  574. value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '',
  575. });
  576. options.push({
  577. key: 'passkey.user_verification',
  578. value:
  579. formValues['passkey.user_verification'] ||
  580. inputs['passkey.user_verification'] ||
  581. 'preferred',
  582. });
  583. options.push({
  584. key: 'passkey.attachment_preference',
  585. value:
  586. formValues['passkey.attachment_preference'] ||
  587. inputs['passkey.attachment_preference'] ||
  588. '',
  589. });
  590. options.push({
  591. key: 'passkey.origins',
  592. value: formValues['passkey.origins'] || inputs['passkey.origins'] || '',
  593. });
  594. await updateOptions(options);
  595. };
  596. const handleCheckboxChange = async (optionKey, event) => {
  597. const value = event.target.checked;
  598. if (optionKey === 'PasswordLoginEnabled' && !value) {
  599. setShowPasswordLoginConfirmModal(true);
  600. } else {
  601. await updateOptions([{ key: optionKey, value }]);
  602. }
  603. if (optionKey === 'LinuxDOOAuthEnabled') {
  604. setLinuxDOOAuthEnabled(value);
  605. }
  606. };
  607. const handlePasswordLoginConfirm = async () => {
  608. await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]);
  609. setShowPasswordLoginConfirmModal(false);
  610. };
  611. return (
  612. <div>
  613. {isLoaded ? (
  614. <Form
  615. initValues={inputs}
  616. onValueChange={handleFormChange}
  617. getFormApi={(api) => (formApiRef.current = api)}
  618. >
  619. {({ formState, values, formApi }) => (
  620. <div
  621. style={{
  622. display: 'flex',
  623. flexDirection: 'column',
  624. gap: '10px',
  625. marginTop: '10px',
  626. }}
  627. >
  628. <Card>
  629. <Form.Section text={t('通用设置')}>
  630. <Row
  631. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  632. >
  633. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  634. <Form.Input
  635. field='ServerAddress'
  636. label={t('服务器地址')}
  637. placeholder='https://yourdomain.com'
  638. extraText={t(
  639. '该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置',
  640. )}
  641. />
  642. </Col>
  643. </Row>
  644. <Button onClick={submitServerAddress}>
  645. {t('更新服务器地址')}
  646. </Button>
  647. </Form.Section>
  648. </Card>
  649. <Card>
  650. <Form.Section text={t('代理设置')}>
  651. <Text>
  652. (支持{' '}
  653. <a
  654. href='https://github.com/Calcium-Ion/new-api-worker'
  655. target='_blank'
  656. rel='noreferrer'
  657. >
  658. new-api-worker
  659. </a>
  660. </Text>
  661. <Row
  662. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  663. >
  664. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  665. <Form.Input
  666. field='WorkerUrl'
  667. label={t('Worker地址')}
  668. placeholder='例如:https://workername.yourdomain.workers.dev'
  669. />
  670. </Col>
  671. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  672. <Form.Input
  673. field='WorkerValidKey'
  674. label={t('Worker密钥')}
  675. placeholder='敏感信息不会发送到前端显示'
  676. type='password'
  677. />
  678. </Col>
  679. </Row>
  680. <Form.Checkbox
  681. field='WorkerAllowHttpImageRequestEnabled'
  682. noLabel
  683. >
  684. {t('允许 HTTP 协议图片请求(适用于自部署代理)')}
  685. </Form.Checkbox>
  686. <Button onClick={submitWorker}>{t('更新Worker设置')}</Button>
  687. </Form.Section>
  688. </Card>
  689. <Card>
  690. <Form.Section text={t('SSRF防护设置')}>
  691. <Text extraText={t('SSRF防护详细说明')}>
  692. {t('配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全')}
  693. </Text>
  694. <Row
  695. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  696. >
  697. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  698. <Form.Checkbox
  699. field='fetch_setting.enable_ssrf_protection'
  700. noLabel
  701. extraText={t('SSRF防护开关详细说明')}
  702. onChange={(e) =>
  703. handleCheckboxChange(
  704. 'fetch_setting.enable_ssrf_protection',
  705. e,
  706. )
  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(
  724. 'fetch_setting.allow_private_ip',
  725. e,
  726. )
  727. }
  728. >
  729. {t(
  730. '允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)',
  731. )}
  732. </Form.Checkbox>
  733. </Col>
  734. </Row>
  735. <Row
  736. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  737. style={{ marginTop: 16 }}
  738. >
  739. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  740. <Form.Checkbox
  741. field='fetch_setting.apply_ip_filter_for_domain'
  742. noLabel
  743. extraText={t('域名IP过滤详细说明')}
  744. onChange={(e) =>
  745. handleCheckboxChange(
  746. 'fetch_setting.apply_ip_filter_for_domain',
  747. e,
  748. )
  749. }
  750. style={{ marginBottom: 8 }}
  751. >
  752. {t('对域名启用 IP 过滤(实验性)')}
  753. </Form.Checkbox>
  754. <Text strong>
  755. {t(domainFilterMode ? '域名白名单' : '域名黑名单')}
  756. </Text>
  757. <Text
  758. type='secondary'
  759. style={{ display: 'block', marginBottom: 8 }}
  760. >
  761. {t(
  762. '支持通配符格式,如:example.com, *.api.example.com',
  763. )}
  764. </Text>
  765. <Radio.Group
  766. type='button'
  767. value={domainFilterMode ? 'whitelist' : 'blacklist'}
  768. onChange={(val) => {
  769. const selected =
  770. val && val.target ? val.target.value : val;
  771. const isWhitelist = selected === 'whitelist';
  772. setDomainFilterMode(isWhitelist);
  773. setInputs((prev) => ({
  774. ...prev,
  775. 'fetch_setting.domain_filter_mode': isWhitelist,
  776. }));
  777. }}
  778. style={{ marginBottom: 8 }}
  779. >
  780. <Radio value='whitelist'>{t('白名单')}</Radio>
  781. <Radio value='blacklist'>{t('黑名单')}</Radio>
  782. </Radio.Group>
  783. <TagInput
  784. value={domainList}
  785. onChange={(value) => {
  786. setDomainList(value);
  787. // 触发Form的onChange事件
  788. setInputs((prev) => ({
  789. ...prev,
  790. 'fetch_setting.domain_list': value,
  791. }));
  792. }}
  793. placeholder={t('输入域名后回车,如:example.com')}
  794. style={{ width: '100%' }}
  795. />
  796. </Col>
  797. </Row>
  798. <Row
  799. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  800. style={{ marginTop: 16 }}
  801. >
  802. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  803. <Text strong>
  804. {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')}
  805. </Text>
  806. <Text
  807. type='secondary'
  808. style={{ display: 'block', marginBottom: 8 }}
  809. >
  810. {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')}
  811. </Text>
  812. <Radio.Group
  813. type='button'
  814. value={ipFilterMode ? 'whitelist' : 'blacklist'}
  815. onChange={(val) => {
  816. const selected =
  817. val && val.target ? val.target.value : val;
  818. const isWhitelist = selected === 'whitelist';
  819. setIpFilterMode(isWhitelist);
  820. setInputs((prev) => ({
  821. ...prev,
  822. 'fetch_setting.ip_filter_mode': isWhitelist,
  823. }));
  824. }}
  825. style={{ marginBottom: 8 }}
  826. >
  827. <Radio value='whitelist'>{t('白名单')}</Radio>
  828. <Radio value='blacklist'>{t('黑名单')}</Radio>
  829. </Radio.Group>
  830. <TagInput
  831. value={ipList}
  832. onChange={(value) => {
  833. setIpList(value);
  834. // 触发Form的onChange事件
  835. setInputs((prev) => ({
  836. ...prev,
  837. 'fetch_setting.ip_list': value,
  838. }));
  839. }}
  840. placeholder={t('输入IP地址后回车,如:8.8.8.8')}
  841. style={{ width: '100%' }}
  842. />
  843. </Col>
  844. </Row>
  845. <Row
  846. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  847. style={{ marginTop: 16 }}
  848. >
  849. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  850. <Text strong>{t('允许的端口')}</Text>
  851. <Text
  852. type='secondary'
  853. style={{ display: 'block', marginBottom: 8 }}
  854. >
  855. {t('支持单个端口和端口范围,如:80, 443, 8000-8999')}
  856. </Text>
  857. <TagInput
  858. value={allowedPorts}
  859. onChange={(value) => {
  860. setAllowedPorts(value);
  861. // 触发Form的onChange事件
  862. setInputs((prev) => ({
  863. ...prev,
  864. 'fetch_setting.allowed_ports': value,
  865. }));
  866. }}
  867. placeholder={t('输入端口后回车,如:80 或 8000-8999')}
  868. style={{ width: '100%' }}
  869. />
  870. <Text
  871. type='secondary'
  872. style={{ display: 'block', marginBottom: 8 }}
  873. >
  874. {t('端口配置详细说明')}
  875. </Text>
  876. </Col>
  877. </Row>
  878. <Button onClick={submitSSRF} style={{ marginTop: 16 }}>
  879. {t('更新SSRF防护设置')}
  880. </Button>
  881. </Form.Section>
  882. </Card>
  883. <Card>
  884. <Form.Section text={t('配置登录注册')}>
  885. <Row
  886. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  887. >
  888. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  889. <Form.Checkbox
  890. field='PasswordLoginEnabled'
  891. noLabel
  892. onChange={(e) =>
  893. handleCheckboxChange('PasswordLoginEnabled', e)
  894. }
  895. >
  896. {t('允许通过密码进行登录')}
  897. </Form.Checkbox>
  898. <Form.Checkbox
  899. field='PasswordRegisterEnabled'
  900. noLabel
  901. onChange={(e) =>
  902. handleCheckboxChange('PasswordRegisterEnabled', e)
  903. }
  904. >
  905. {t('允许通过密码进行注册')}
  906. </Form.Checkbox>
  907. <Form.Checkbox
  908. field='EmailVerificationEnabled'
  909. noLabel
  910. onChange={(e) =>
  911. handleCheckboxChange('EmailVerificationEnabled', e)
  912. }
  913. >
  914. {t('通过密码注册时需要进行邮箱验证')}
  915. </Form.Checkbox>
  916. <Form.Checkbox
  917. field='RegisterEnabled'
  918. noLabel
  919. onChange={(e) =>
  920. handleCheckboxChange('RegisterEnabled', e)
  921. }
  922. >
  923. {t('允许新用户注册')}
  924. </Form.Checkbox>
  925. <Form.Checkbox
  926. field='TurnstileCheckEnabled'
  927. noLabel
  928. onChange={(e) =>
  929. handleCheckboxChange('TurnstileCheckEnabled', e)
  930. }
  931. >
  932. {t('允许 Turnstile 用户校验')}
  933. </Form.Checkbox>
  934. </Col>
  935. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  936. <Form.Checkbox
  937. field='GitHubOAuthEnabled'
  938. noLabel
  939. onChange={(e) =>
  940. handleCheckboxChange('GitHubOAuthEnabled', e)
  941. }
  942. >
  943. {t('允许通过 GitHub 账户登录 & 注册')}
  944. </Form.Checkbox>
  945. <Form.Checkbox
  946. field='LinuxDOOAuthEnabled'
  947. noLabel
  948. onChange={(e) =>
  949. handleCheckboxChange('LinuxDOOAuthEnabled', e)
  950. }
  951. >
  952. {t('允许通过 Linux DO 账户登录 & 注册')}
  953. </Form.Checkbox>
  954. <Form.Checkbox
  955. field='WeChatAuthEnabled'
  956. noLabel
  957. onChange={(e) =>
  958. handleCheckboxChange('WeChatAuthEnabled', e)
  959. }
  960. >
  961. {t('允许通过微信登录 & 注册')}
  962. </Form.Checkbox>
  963. <Form.Checkbox
  964. field='TelegramOAuthEnabled'
  965. noLabel
  966. onChange={(e) =>
  967. handleCheckboxChange('TelegramOAuthEnabled', e)
  968. }
  969. >
  970. {t('允许通过 Telegram 进行登录')}
  971. </Form.Checkbox>
  972. <Form.Checkbox
  973. field="['oidc.enabled']"
  974. noLabel
  975. onChange={(e) =>
  976. handleCheckboxChange('oidc.enabled', e)
  977. }
  978. >
  979. {t('允许通过 OIDC 进行登录')}
  980. </Form.Checkbox>
  981. </Col>
  982. </Row>
  983. </Form.Section>
  984. </Card>
  985. <Card>
  986. <Form.Section text={t('配置 Passkey')}>
  987. <Text>{t('用以支持基于 WebAuthn 的无密码登录注册')}</Text>
  988. <Banner
  989. type='info'
  990. description={t(
  991. 'Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式',
  992. )}
  993. style={{ marginBottom: 20, marginTop: 16 }}
  994. />
  995. <Row
  996. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  997. >
  998. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  999. <Form.Checkbox
  1000. field="['passkey.enabled']"
  1001. noLabel
  1002. onChange={(e) =>
  1003. handleCheckboxChange('passkey.enabled', e)
  1004. }
  1005. >
  1006. {t('允许通过 Passkey 登录 & 认证')}
  1007. </Form.Checkbox>
  1008. </Col>
  1009. </Row>
  1010. <Row
  1011. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1012. >
  1013. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1014. <Form.Input
  1015. field="['passkey.rp_display_name']"
  1016. label={t('服务显示名称')}
  1017. placeholder={t('默认使用系统名称')}
  1018. extraText={t(
  1019. "用户注册时看到的网站名称,比如'我的网站'",
  1020. )}
  1021. />
  1022. </Col>
  1023. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1024. <Form.Input
  1025. field="['passkey.rp_id']"
  1026. label={t('网站域名标识')}
  1027. placeholder={t('例如:example.com')}
  1028. extraText={t(
  1029. '留空则默认使用服务器地址,注意不能携带http://或者https://',
  1030. )}
  1031. />
  1032. </Col>
  1033. </Row>
  1034. <Row
  1035. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1036. style={{ marginTop: 16 }}
  1037. >
  1038. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1039. <Form.Select
  1040. field="['passkey.user_verification']"
  1041. label={t('安全验证级别')}
  1042. placeholder={t('是否要求指纹/面容等生物识别')}
  1043. optionList={[
  1044. {
  1045. label: t('推荐使用(用户可选)'),
  1046. value: 'preferred',
  1047. },
  1048. { label: t('强制要求'), value: 'required' },
  1049. { label: t('不建议使用'), value: 'discouraged' },
  1050. ]}
  1051. extraText={t('推荐:用户可以选择是否使用指纹等验证')}
  1052. />
  1053. </Col>
  1054. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1055. <Form.Select
  1056. field="['passkey.attachment_preference']"
  1057. label={t('设备类型偏好')}
  1058. placeholder={t('选择支持的认证设备类型')}
  1059. optionList={[
  1060. { label: t('不限制'), value: '' },
  1061. { label: t('本设备内置'), value: 'platform' },
  1062. { label: t('外接设备'), value: 'cross-platform' },
  1063. ]}
  1064. extraText={t(
  1065. '本设备:手机指纹/面容,外接:USB安全密钥',
  1066. )}
  1067. />
  1068. </Col>
  1069. </Row>
  1070. <Row
  1071. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1072. style={{ marginTop: 16 }}
  1073. >
  1074. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  1075. <Form.Checkbox
  1076. field="['passkey.allow_insecure_origin']"
  1077. noLabel
  1078. extraText={t('仅用于开发环境,生产环境应使用 HTTPS')}
  1079. onChange={(e) =>
  1080. handleCheckboxChange(
  1081. 'passkey.allow_insecure_origin',
  1082. e,
  1083. )
  1084. }
  1085. >
  1086. {t('允许不安全的 Origin(HTTP)')}
  1087. </Form.Checkbox>
  1088. </Col>
  1089. </Row>
  1090. <Row
  1091. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1092. style={{ marginTop: 16 }}
  1093. >
  1094. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  1095. <Form.Input
  1096. field="['passkey.origins']"
  1097. label={t('允许的 Origins')}
  1098. placeholder={t('填写带https的域名,逗号分隔')}
  1099. extraText={t(
  1100. '为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https',
  1101. )}
  1102. />
  1103. </Col>
  1104. </Row>
  1105. <Button
  1106. onClick={submitPasskeySettings}
  1107. style={{ marginTop: 16 }}
  1108. >
  1109. {t('保存 Passkey 设置')}
  1110. </Button>
  1111. </Form.Section>
  1112. </Card>
  1113. <Card>
  1114. <Form.Section text={t('配置邮箱域名白名单')}>
  1115. <Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text>
  1116. <Row
  1117. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1118. >
  1119. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1120. <Form.Checkbox
  1121. field='EmailDomainRestrictionEnabled'
  1122. noLabel
  1123. onChange={(e) =>
  1124. handleCheckboxChange(
  1125. 'EmailDomainRestrictionEnabled',
  1126. e,
  1127. )
  1128. }
  1129. >
  1130. 启用邮箱域名白名单
  1131. </Form.Checkbox>
  1132. </Col>
  1133. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1134. <Form.Checkbox
  1135. field='EmailAliasRestrictionEnabled'
  1136. noLabel
  1137. onChange={(e) =>
  1138. handleCheckboxChange(
  1139. 'EmailAliasRestrictionEnabled',
  1140. e,
  1141. )
  1142. }
  1143. >
  1144. 启用邮箱别名限制
  1145. </Form.Checkbox>
  1146. </Col>
  1147. </Row>
  1148. <TagInput
  1149. value={emailDomainWhitelist}
  1150. onChange={setEmailDomainWhitelist}
  1151. placeholder={t('输入域名后回车')}
  1152. style={{ width: '100%', marginTop: 16 }}
  1153. />
  1154. <Form.Input
  1155. placeholder={t('输入要添加的邮箱域名')}
  1156. value={emailToAdd}
  1157. onChange={(value) => setEmailToAdd(value)}
  1158. style={{ marginTop: 16 }}
  1159. suffix={
  1160. <Button
  1161. theme='solid'
  1162. type='primary'
  1163. onClick={handleAddEmail}
  1164. >
  1165. {t('添加')}
  1166. </Button>
  1167. }
  1168. onEnterPress={handleAddEmail}
  1169. />
  1170. <Button
  1171. onClick={submitEmailDomainWhitelist}
  1172. style={{ marginTop: 10 }}
  1173. >
  1174. {t('保存邮箱域名白名单设置')}
  1175. </Button>
  1176. </Form.Section>
  1177. </Card>
  1178. <Card>
  1179. <Form.Section text={t('配置 SMTP')}>
  1180. <Text>{t('用以支持系统的邮件发送')}</Text>
  1181. <Row
  1182. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1183. >
  1184. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1185. <Form.Input
  1186. field='SMTPServer'
  1187. label={t('SMTP 服务器地址')}
  1188. />
  1189. </Col>
  1190. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1191. <Form.Input field='SMTPPort' label={t('SMTP 端口')} />
  1192. </Col>
  1193. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1194. <Form.Input field='SMTPAccount' label={t('SMTP 账户')} />
  1195. </Col>
  1196. </Row>
  1197. <Row
  1198. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1199. style={{ marginTop: 16 }}
  1200. >
  1201. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1202. <Form.Input
  1203. field='SMTPFrom'
  1204. label={t('SMTP 发送者邮箱')}
  1205. />
  1206. </Col>
  1207. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1208. <Form.Input
  1209. field='SMTPToken'
  1210. label={t('SMTP 访问凭证')}
  1211. type='password'
  1212. placeholder='敏感信息不会发送到前端显示'
  1213. />
  1214. </Col>
  1215. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1216. <Form.Checkbox
  1217. field='SMTPSSLEnabled'
  1218. noLabel
  1219. onChange={(e) =>
  1220. handleCheckboxChange('SMTPSSLEnabled', e)
  1221. }
  1222. >
  1223. {t('启用SMTP SSL')}
  1224. </Form.Checkbox>
  1225. </Col>
  1226. </Row>
  1227. <Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button>
  1228. </Form.Section>
  1229. </Card>
  1230. <Card>
  1231. <Form.Section text={t('配置 OIDC')}>
  1232. <Text>
  1233. {t(
  1234. '用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP',
  1235. )}
  1236. </Text>
  1237. <Banner
  1238. type='info'
  1239. description={`${t('主页链接填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('重定向 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/oidc`}
  1240. style={{ marginBottom: 20, marginTop: 16 }}
  1241. />
  1242. <Text>
  1243. {t(
  1244. '若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置',
  1245. )}
  1246. </Text>
  1247. <Row
  1248. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1249. >
  1250. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1251. <Form.Input
  1252. field="['oidc.well_known']"
  1253. label={t('Well-Known URL')}
  1254. placeholder={t('请输入 OIDC 的 Well-Known URL')}
  1255. />
  1256. </Col>
  1257. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1258. <Form.Input
  1259. field="['oidc.client_id']"
  1260. label={t('Client ID')}
  1261. placeholder={t('输入 OIDC 的 Client ID')}
  1262. />
  1263. </Col>
  1264. </Row>
  1265. <Row
  1266. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1267. >
  1268. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1269. <Form.Input
  1270. field="['oidc.client_secret']"
  1271. label={t('Client Secret')}
  1272. type='password'
  1273. placeholder={t('敏感信息不会发送到前端显示')}
  1274. />
  1275. </Col>
  1276. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1277. <Form.Input
  1278. field="['oidc.authorization_endpoint']"
  1279. label={t('Authorization Endpoint')}
  1280. placeholder={t('输入 OIDC 的 Authorization Endpoint')}
  1281. />
  1282. </Col>
  1283. </Row>
  1284. <Row
  1285. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1286. >
  1287. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1288. <Form.Input
  1289. field="['oidc.token_endpoint']"
  1290. label={t('Token Endpoint')}
  1291. placeholder={t('输入 OIDC 的 Token Endpoint')}
  1292. />
  1293. </Col>
  1294. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1295. <Form.Input
  1296. field="['oidc.user_info_endpoint']"
  1297. label={t('User Info Endpoint')}
  1298. placeholder={t('输入 OIDC 的 Userinfo Endpoint')}
  1299. />
  1300. </Col>
  1301. </Row>
  1302. <Button onClick={submitOIDCSettings}>
  1303. {t('保存 OIDC 设置')}
  1304. </Button>
  1305. </Form.Section>
  1306. </Card>
  1307. <Card>
  1308. <Form.Section text={t('配置 GitHub OAuth App')}>
  1309. <Text>{t('用以支持通过 GitHub 进行登录注册')}</Text>
  1310. <Banner
  1311. type='info'
  1312. description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/github`}
  1313. style={{ marginBottom: 20, marginTop: 16 }}
  1314. />
  1315. <Row
  1316. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1317. >
  1318. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1319. <Form.Input
  1320. field='GitHubClientId'
  1321. label={t('GitHub Client ID')}
  1322. />
  1323. </Col>
  1324. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1325. <Form.Input
  1326. field='GitHubClientSecret'
  1327. label={t('GitHub Client Secret')}
  1328. type='password'
  1329. placeholder={t('敏感信息不会发送到前端显示')}
  1330. />
  1331. </Col>
  1332. </Row>
  1333. <Button onClick={submitGitHubOAuth}>
  1334. {t('保存 GitHub OAuth 设置')}
  1335. </Button>
  1336. </Form.Section>
  1337. </Card>
  1338. <Card>
  1339. <Form.Section text={t('配置 Linux DO OAuth')}>
  1340. <Text>
  1341. {t('用以支持通过 Linux DO 进行登录注册')}
  1342. <a
  1343. href='https://connect.linux.do/'
  1344. target='_blank'
  1345. rel='noreferrer'
  1346. style={{
  1347. display: 'inline-block',
  1348. marginLeft: 4,
  1349. marginRight: 4,
  1350. }}
  1351. >
  1352. {t('点击此处')}
  1353. </a>
  1354. {t('管理你的 LinuxDO OAuth App')}
  1355. </Text>
  1356. <Banner
  1357. type='info'
  1358. description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/linuxdo`}
  1359. style={{ marginBottom: 20, marginTop: 16 }}
  1360. />
  1361. <Row
  1362. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1363. >
  1364. <Col xs={24} sm={24} md={10} lg={10} xl={10}>
  1365. <Form.Input
  1366. field='LinuxDOClientId'
  1367. label={t('Linux DO Client ID')}
  1368. placeholder={t('输入你注册的 LinuxDO OAuth APP 的 ID')}
  1369. />
  1370. </Col>
  1371. <Col xs={24} sm={24} md={10} lg={10} xl={10}>
  1372. <Form.Input
  1373. field='LinuxDOClientSecret'
  1374. label={t('Linux DO Client Secret')}
  1375. type='password'
  1376. placeholder={t('敏感信息不会发送到前端显示')}
  1377. />
  1378. </Col>
  1379. <Col xs={24} sm={24} md={4} lg={4} xl={4}>
  1380. <Form.Input
  1381. field='LinuxDOMinimumTrustLevel'
  1382. label='LinuxDO Minimum Trust Level'
  1383. placeholder='允许注册的最低信任等级'
  1384. />
  1385. </Col>
  1386. </Row>
  1387. <Button onClick={submitLinuxDOOAuth}>
  1388. {t('保存 Linux DO OAuth 设置')}
  1389. </Button>
  1390. </Form.Section>
  1391. </Card>
  1392. <Card>
  1393. <Form.Section text={t('配置 WeChat Server')}>
  1394. <Text>{t('用以支持通过微信进行登录注册')}</Text>
  1395. <Row
  1396. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1397. >
  1398. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1399. <Form.Input
  1400. field='WeChatServerAddress'
  1401. label={t('WeChat Server 服务器地址')}
  1402. />
  1403. </Col>
  1404. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1405. <Form.Input
  1406. field='WeChatServerToken'
  1407. label={t('WeChat Server 访问凭证')}
  1408. type='password'
  1409. placeholder={t('敏感信息不会发送到前端显示')}
  1410. />
  1411. </Col>
  1412. <Col xs={24} sm={24} md={8} lg={8} xl={8}>
  1413. <Form.Input
  1414. field='WeChatAccountQRCodeImageURL'
  1415. label={t('微信公众号二维码图片链接')}
  1416. />
  1417. </Col>
  1418. </Row>
  1419. <Button onClick={submitWeChat}>
  1420. {t('保存 WeChat Server 设置')}
  1421. </Button>
  1422. </Form.Section>
  1423. </Card>
  1424. <Card>
  1425. <Form.Section text={t('配置 Telegram 登录')}>
  1426. <Text>{t('用以支持通过 Telegram 进行登录注册')}</Text>
  1427. <Row
  1428. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1429. >
  1430. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1431. <Form.Input
  1432. field='TelegramBotToken'
  1433. label={t('Telegram Bot Token')}
  1434. placeholder={t('敏感信息不会发送到前端显示')}
  1435. type='password'
  1436. />
  1437. </Col>
  1438. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1439. <Form.Input
  1440. field='TelegramBotName'
  1441. label={t('Telegram Bot 名称')}
  1442. />
  1443. </Col>
  1444. </Row>
  1445. <Button onClick={submitTelegramSettings}>
  1446. {t('保存 Telegram 登录设置')}
  1447. </Button>
  1448. </Form.Section>
  1449. </Card>
  1450. <Card>
  1451. <Form.Section text={t('配置 Turnstile')}>
  1452. <Text>{t('用以支持用户校验')}</Text>
  1453. <Row
  1454. gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
  1455. >
  1456. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1457. <Form.Input
  1458. field='TurnstileSiteKey'
  1459. label={t('Turnstile Site Key')}
  1460. />
  1461. </Col>
  1462. <Col xs={24} sm={24} md={12} lg={12} xl={12}>
  1463. <Form.Input
  1464. field='TurnstileSecretKey'
  1465. label={t('Turnstile Secret Key')}
  1466. type='password'
  1467. placeholder={t('敏感信息不会发送到前端显示')}
  1468. />
  1469. </Col>
  1470. </Row>
  1471. <Button onClick={submitTurnstile}>
  1472. {t('保存 Turnstile 设置')}
  1473. </Button>
  1474. </Form.Section>
  1475. </Card>
  1476. <Modal
  1477. title={t('确认取消密码登录')}
  1478. visible={showPasswordLoginConfirmModal}
  1479. onOk={handlePasswordLoginConfirm}
  1480. onCancel={() => {
  1481. setShowPasswordLoginConfirmModal(false);
  1482. formApiRef.current.setValue('PasswordLoginEnabled', true);
  1483. }}
  1484. okText={t('确认')}
  1485. cancelText={t('取消')}
  1486. >
  1487. <p>
  1488. {t(
  1489. '您确定要取消密码登录功能吗?这可能会影响用户的登录方式。',
  1490. )}
  1491. </p>
  1492. </Modal>
  1493. </div>
  1494. )}
  1495. </Form>
  1496. ) : (
  1497. <div
  1498. style={{
  1499. display: 'flex',
  1500. justifyContent: 'center',
  1501. alignItems: 'center',
  1502. height: '100vh',
  1503. }}
  1504. >
  1505. <Spin size='large' />
  1506. </div>
  1507. )}
  1508. </div>
  1509. );
  1510. };
  1511. export default SystemSetting;