| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import React, { useEffect, useState, useRef } from 'react'; |
| | import { |
| | Button, |
| | Form, |
| | Row, |
| | Col, |
| | Typography, |
| | Modal, |
| | Banner, |
| | TagInput, |
| | Spin, |
| | Card, |
| | Radio, |
| | Select, |
| | } from '@douyinfe/semi-ui'; |
| | const { Text } = Typography; |
| | import { |
| | API, |
| | removeTrailingSlash, |
| | showError, |
| | showSuccess, |
| | toBoolean, |
| | } from '../../helpers'; |
| | import axios from 'axios'; |
| | import { useTranslation } from 'react-i18next'; |
| |
|
| | const SystemSetting = () => { |
| | const { t } = useTranslation(); |
| | let [inputs, setInputs] = useState({ |
| | PasswordLoginEnabled: '', |
| | PasswordRegisterEnabled: '', |
| | EmailVerificationEnabled: '', |
| | GitHubOAuthEnabled: '', |
| | GitHubClientId: '', |
| | GitHubClientSecret: '', |
| | 'discord.enabled': '', |
| | 'discord.client_id': '', |
| | 'discord.client_secret': '', |
| | 'oidc.enabled': '', |
| | 'oidc.client_id': '', |
| | 'oidc.client_secret': '', |
| | 'oidc.well_known': '', |
| | 'oidc.authorization_endpoint': '', |
| | 'oidc.token_endpoint': '', |
| | 'oidc.user_info_endpoint': '', |
| | Notice: '', |
| | SMTPServer: '', |
| | SMTPPort: '', |
| | SMTPAccount: '', |
| | SMTPFrom: '', |
| | SMTPToken: '', |
| | WorkerUrl: '', |
| | WorkerValidKey: '', |
| | WorkerAllowHttpImageRequestEnabled: '', |
| | Footer: '', |
| | WeChatAuthEnabled: '', |
| | WeChatServerAddress: '', |
| | WeChatServerToken: '', |
| | WeChatAccountQRCodeImageURL: '', |
| | TurnstileCheckEnabled: '', |
| | TurnstileSiteKey: '', |
| | TurnstileSecretKey: '', |
| | RegisterEnabled: '', |
| | 'passkey.enabled': '', |
| | 'passkey.rp_display_name': '', |
| | 'passkey.rp_id': '', |
| | 'passkey.origins': [], |
| | 'passkey.allow_insecure_origin': '', |
| | 'passkey.user_verification': 'preferred', |
| | 'passkey.attachment_preference': '', |
| | EmailDomainRestrictionEnabled: '', |
| | EmailAliasRestrictionEnabled: '', |
| | SMTPSSLEnabled: '', |
| | EmailDomainWhitelist: [], |
| | TelegramOAuthEnabled: '', |
| | TelegramBotToken: '', |
| | TelegramBotName: '', |
| | LinuxDOOAuthEnabled: '', |
| | LinuxDOClientId: '', |
| | LinuxDOClientSecret: '', |
| | LinuxDOMinimumTrustLevel: '', |
| | ServerAddress: '', |
| | |
| | 'fetch_setting.enable_ssrf_protection': true, |
| | 'fetch_setting.allow_private_ip': '', |
| | 'fetch_setting.domain_filter_mode': false, |
| | 'fetch_setting.ip_filter_mode': false, |
| | 'fetch_setting.domain_list': [], |
| | 'fetch_setting.ip_list': [], |
| | 'fetch_setting.allowed_ports': [], |
| | 'fetch_setting.apply_ip_filter_for_domain': false, |
| | }); |
| |
|
| | const [originInputs, setOriginInputs] = useState({}); |
| | const [loading, setLoading] = useState(false); |
| | const [isLoaded, setIsLoaded] = useState(false); |
| | const formApiRef = useRef(null); |
| | const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]); |
| | const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] = |
| | useState(false); |
| | const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false); |
| | const [emailToAdd, setEmailToAdd] = useState(''); |
| | const [domainFilterMode, setDomainFilterMode] = useState(true); |
| | const [ipFilterMode, setIpFilterMode] = useState(true); |
| | const [domainList, setDomainList] = useState([]); |
| | const [ipList, setIpList] = useState([]); |
| | const [allowedPorts, setAllowedPorts] = useState([]); |
| |
|
| | const getOptions = async () => { |
| | setLoading(true); |
| | const res = await API.get('/api/option/'); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | let newInputs = {}; |
| | data.forEach((item) => { |
| | switch (item.key) { |
| | case 'TopupGroupRatio': |
| | item.value = JSON.stringify(JSON.parse(item.value), null, 2); |
| | break; |
| | case 'EmailDomainWhitelist': |
| | setEmailDomainWhitelist(item.value ? item.value.split(',') : []); |
| | break; |
| | case 'fetch_setting.allow_private_ip': |
| | case 'fetch_setting.enable_ssrf_protection': |
| | case 'fetch_setting.domain_filter_mode': |
| | case 'fetch_setting.ip_filter_mode': |
| | case 'fetch_setting.apply_ip_filter_for_domain': |
| | item.value = toBoolean(item.value); |
| | break; |
| | case 'fetch_setting.domain_list': |
| | try { |
| | const domains = item.value ? JSON.parse(item.value) : []; |
| | setDomainList(Array.isArray(domains) ? domains : []); |
| | } catch (e) { |
| | setDomainList([]); |
| | } |
| | break; |
| | case 'fetch_setting.ip_list': |
| | try { |
| | const ips = item.value ? JSON.parse(item.value) : []; |
| | setIpList(Array.isArray(ips) ? ips : []); |
| | } catch (e) { |
| | setIpList([]); |
| | } |
| | break; |
| | case 'fetch_setting.allowed_ports': |
| | try { |
| | const ports = item.value ? JSON.parse(item.value) : []; |
| | setAllowedPorts(Array.isArray(ports) ? ports : []); |
| | } catch (e) { |
| | setAllowedPorts(['80', '443', '8080', '8443']); |
| | } |
| | break; |
| | case 'PasswordLoginEnabled': |
| | case 'PasswordRegisterEnabled': |
| | case 'EmailVerificationEnabled': |
| | case 'GitHubOAuthEnabled': |
| | case 'WeChatAuthEnabled': |
| | case 'TelegramOAuthEnabled': |
| | case 'RegisterEnabled': |
| | case 'TurnstileCheckEnabled': |
| | case 'EmailDomainRestrictionEnabled': |
| | case 'EmailAliasRestrictionEnabled': |
| | case 'SMTPSSLEnabled': |
| | case 'LinuxDOOAuthEnabled': |
| | case 'discord.enabled': |
| | case 'oidc.enabled': |
| | case 'passkey.enabled': |
| | case 'passkey.allow_insecure_origin': |
| | case 'WorkerAllowHttpImageRequestEnabled': |
| | item.value = toBoolean(item.value); |
| | break; |
| | case 'passkey.origins': |
| | |
| | item.value = item.value || ''; |
| | break; |
| | case 'passkey.rp_display_name': |
| | case 'passkey.rp_id': |
| | case 'passkey.attachment_preference': |
| | |
| | item.value = item.value || ''; |
| | break; |
| | case 'passkey.user_verification': |
| | |
| | item.value = item.value || 'preferred'; |
| | break; |
| | case 'Price': |
| | case 'MinTopUp': |
| | item.value = parseFloat(item.value); |
| | break; |
| | default: |
| | break; |
| | } |
| | newInputs[item.key] = item.value; |
| | }); |
| | setInputs(newInputs); |
| | setOriginInputs(newInputs); |
| | |
| | if ( |
| | typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined' |
| | ) { |
| | setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']); |
| | } |
| | if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') { |
| | setIpFilterMode(!!newInputs['fetch_setting.ip_filter_mode']); |
| | } |
| | if (formApiRef.current) { |
| | formApiRef.current.setValues(newInputs); |
| | } |
| | setIsLoaded(true); |
| | } else { |
| | showError(message); |
| | } |
| | setLoading(false); |
| | }; |
| |
|
| | useEffect(() => { |
| | getOptions(); |
| | }, []); |
| |
|
| | const updateOptions = async (options) => { |
| | setLoading(true); |
| | try { |
| | |
| | const checkboxOptions = options.filter((opt) => |
| | opt.key.toLowerCase().endsWith('enabled'), |
| | ); |
| | const otherOptions = options.filter( |
| | (opt) => !opt.key.toLowerCase().endsWith('enabled'), |
| | ); |
| |
|
| | |
| | for (const opt of checkboxOptions) { |
| | const res = await API.put('/api/option/', { |
| | key: opt.key, |
| | value: opt.value.toString(), |
| | }); |
| | if (!res.data.success) { |
| | showError(res.data.message); |
| | return; |
| | } |
| | } |
| |
|
| | |
| | if (otherOptions.length > 0) { |
| | const requestQueue = otherOptions.map((opt) => |
| | API.put('/api/option/', { |
| | key: opt.key, |
| | value: |
| | typeof opt.value === 'boolean' ? opt.value.toString() : opt.value, |
| | }), |
| | ); |
| |
|
| | const results = await Promise.all(requestQueue); |
| |
|
| | |
| | const errorResults = results.filter((res) => !res.data.success); |
| | errorResults.forEach((res) => { |
| | showError(res.data.message); |
| | }); |
| | } |
| |
|
| | showSuccess(t('更新成功')); |
| | |
| | const newInputs = { ...inputs }; |
| | options.forEach((opt) => { |
| | newInputs[opt.key] = opt.value; |
| | }); |
| | setInputs(newInputs); |
| | } catch (error) { |
| | showError(t('更新失败')); |
| | } |
| | setLoading(false); |
| | }; |
| |
|
| | const handleFormChange = (values) => { |
| | setInputs(values); |
| | }; |
| |
|
| | const submitWorker = async () => { |
| | let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl); |
| | const options = [ |
| | { key: 'WorkerUrl', value: WorkerUrl }, |
| | { |
| | key: 'WorkerAllowHttpImageRequestEnabled', |
| | value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false', |
| | }, |
| | ]; |
| | if (inputs.WorkerValidKey !== '' || WorkerUrl === '') { |
| | options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey }); |
| | } |
| | await updateOptions(options); |
| | }; |
| |
|
| | const submitServerAddress = async () => { |
| | let ServerAddress = removeTrailingSlash(inputs.ServerAddress); |
| | await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]); |
| | }; |
| |
|
| | const submitSMTP = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['SMTPServer'] !== inputs.SMTPServer) { |
| | options.push({ key: 'SMTPServer', value: inputs.SMTPServer }); |
| | } |
| | if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { |
| | options.push({ key: 'SMTPAccount', value: inputs.SMTPAccount }); |
| | } |
| | if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { |
| | options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom }); |
| | } |
| | if ( |
| | originInputs['SMTPPort'] !== inputs.SMTPPort && |
| | inputs.SMTPPort !== '' |
| | ) { |
| | options.push({ key: 'SMTPPort', value: inputs.SMTPPort }); |
| | } |
| | if ( |
| | originInputs['SMTPToken'] !== inputs.SMTPToken && |
| | inputs.SMTPToken !== '' |
| | ) { |
| | options.push({ key: 'SMTPToken', value: inputs.SMTPToken }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitEmailDomainWhitelist = async () => { |
| | if (Array.isArray(emailDomainWhitelist)) { |
| | await updateOptions([ |
| | { |
| | key: 'EmailDomainWhitelist', |
| | value: emailDomainWhitelist.join(','), |
| | }, |
| | ]); |
| | } else { |
| | showError(t('邮箱域名白名单格式不正确')); |
| | } |
| | }; |
| |
|
| | const submitSSRF = async () => { |
| | const options = []; |
| |
|
| | |
| | options.push({ |
| | key: 'fetch_setting.domain_filter_mode', |
| | value: domainFilterMode, |
| | }); |
| | if (Array.isArray(domainList)) { |
| | options.push({ |
| | key: 'fetch_setting.domain_list', |
| | value: JSON.stringify(domainList), |
| | }); |
| | } |
| |
|
| | |
| | options.push({ |
| | key: 'fetch_setting.ip_filter_mode', |
| | value: ipFilterMode, |
| | }); |
| | if (Array.isArray(ipList)) { |
| | options.push({ |
| | key: 'fetch_setting.ip_list', |
| | value: JSON.stringify(ipList), |
| | }); |
| | } |
| |
|
| | |
| | if (Array.isArray(allowedPorts)) { |
| | options.push({ |
| | key: 'fetch_setting.allowed_ports', |
| | value: JSON.stringify(allowedPorts), |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const handleAddEmail = () => { |
| | if (emailToAdd && emailToAdd.trim() !== '') { |
| | const domain = emailToAdd.trim(); |
| |
|
| | |
| | const domainRegex = |
| | /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; |
| | if (!domainRegex.test(domain)) { |
| | showError(t('邮箱域名格式不正确,请输入有效的域名,如 gmail.com')); |
| | return; |
| | } |
| |
|
| | |
| | if (emailDomainWhitelist.includes(domain)) { |
| | showError(t('该域名已存在于白名单中')); |
| | return; |
| | } |
| |
|
| | setEmailDomainWhitelist([...emailDomainWhitelist, domain]); |
| | setEmailToAdd(''); |
| | showSuccess(t('已添加到白名单')); |
| | } |
| | }; |
| |
|
| | const submitWeChat = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { |
| | options.push({ |
| | key: 'WeChatServerAddress', |
| | value: removeTrailingSlash(inputs.WeChatServerAddress), |
| | }); |
| | } |
| | if ( |
| | originInputs['WeChatAccountQRCodeImageURL'] !== |
| | inputs.WeChatAccountQRCodeImageURL |
| | ) { |
| | options.push({ |
| | key: 'WeChatAccountQRCodeImageURL', |
| | value: inputs.WeChatAccountQRCodeImageURL, |
| | }); |
| | } |
| | if ( |
| | originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && |
| | inputs.WeChatServerToken !== '' |
| | ) { |
| | options.push({ |
| | key: 'WeChatServerToken', |
| | value: inputs.WeChatServerToken, |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitGitHubOAuth = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { |
| | options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId }); |
| | } |
| | if ( |
| | originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && |
| | inputs.GitHubClientSecret !== '' |
| | ) { |
| | options.push({ |
| | key: 'GitHubClientSecret', |
| | value: inputs.GitHubClientSecret, |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitDiscordOAuth = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['discord.client_id'] !== inputs['discord.client_id']) { |
| | options.push({ key: 'discord.client_id', value: inputs['discord.client_id'] }); |
| | } |
| | if ( |
| | originInputs['discord.client_secret'] !== inputs['discord.client_secret'] && |
| | inputs['discord.client_secret'] !== '' |
| | ) { |
| | options.push({ |
| | key: 'discord.client_secret', |
| | value: inputs['discord.client_secret'], |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitOIDCSettings = async () => { |
| | if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') { |
| | if ( |
| | !inputs['oidc.well_known'].startsWith('http://') && |
| | !inputs['oidc.well_known'].startsWith('https://') |
| | ) { |
| | showError(t('Well-Known URL 必须以 http:// 或 https:// 开头')); |
| | return; |
| | } |
| | try { |
| | const res = await axios.create().get(inputs['oidc.well_known']); |
| | inputs['oidc.authorization_endpoint'] = |
| | res.data['authorization_endpoint']; |
| | inputs['oidc.token_endpoint'] = res.data['token_endpoint']; |
| | inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint']; |
| | showSuccess(t('获取 OIDC 配置成功!')); |
| | } catch (err) { |
| | console.error(err); |
| | showError( |
| | t('获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确'), |
| | ); |
| | return; |
| | } |
| | } |
| |
|
| | const options = []; |
| |
|
| | if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) { |
| | options.push({ |
| | key: 'oidc.well_known', |
| | value: inputs['oidc.well_known'], |
| | }); |
| | } |
| | if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) { |
| | options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] }); |
| | } |
| | if ( |
| | originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && |
| | inputs['oidc.client_secret'] !== '' |
| | ) { |
| | options.push({ |
| | key: 'oidc.client_secret', |
| | value: inputs['oidc.client_secret'], |
| | }); |
| | } |
| | if ( |
| | originInputs['oidc.authorization_endpoint'] !== |
| | inputs['oidc.authorization_endpoint'] |
| | ) { |
| | options.push({ |
| | key: 'oidc.authorization_endpoint', |
| | value: inputs['oidc.authorization_endpoint'], |
| | }); |
| | } |
| | if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) { |
| | options.push({ |
| | key: 'oidc.token_endpoint', |
| | value: inputs['oidc.token_endpoint'], |
| | }); |
| | } |
| | if ( |
| | originInputs['oidc.user_info_endpoint'] !== |
| | inputs['oidc.user_info_endpoint'] |
| | ) { |
| | options.push({ |
| | key: 'oidc.user_info_endpoint', |
| | value: inputs['oidc.user_info_endpoint'], |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitTelegramSettings = async () => { |
| | const options = [ |
| | { key: 'TelegramBotToken', value: inputs.TelegramBotToken }, |
| | { key: 'TelegramBotName', value: inputs.TelegramBotName }, |
| | ]; |
| | await updateOptions(options); |
| | }; |
| |
|
| | const submitTurnstile = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { |
| | options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey }); |
| | } |
| | if ( |
| | originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && |
| | inputs.TurnstileSecretKey !== '' |
| | ) { |
| | options.push({ |
| | key: 'TurnstileSecretKey', |
| | value: inputs.TurnstileSecretKey, |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitLinuxDOOAuth = async () => { |
| | const options = []; |
| |
|
| | if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) { |
| | options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId }); |
| | } |
| | if ( |
| | originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret && |
| | inputs.LinuxDOClientSecret !== '' |
| | ) { |
| | options.push({ |
| | key: 'LinuxDOClientSecret', |
| | value: inputs.LinuxDOClientSecret, |
| | }); |
| | } |
| | if ( |
| | originInputs['LinuxDOMinimumTrustLevel'] !== |
| | inputs.LinuxDOMinimumTrustLevel |
| | ) { |
| | options.push({ |
| | key: 'LinuxDOMinimumTrustLevel', |
| | value: inputs.LinuxDOMinimumTrustLevel, |
| | }); |
| | } |
| |
|
| | if (options.length > 0) { |
| | await updateOptions(options); |
| | } |
| | }; |
| |
|
| | const submitPasskeySettings = async () => { |
| | |
| | const formValues = formApiRef.current?.getValues() || {}; |
| |
|
| | const options = []; |
| |
|
| | options.push({ |
| | key: 'passkey.rp_display_name', |
| | value: |
| | formValues['passkey.rp_display_name'] || |
| | inputs['passkey.rp_display_name'] || |
| | '', |
| | }); |
| | options.push({ |
| | key: 'passkey.rp_id', |
| | value: formValues['passkey.rp_id'] || inputs['passkey.rp_id'] || '', |
| | }); |
| | options.push({ |
| | key: 'passkey.user_verification', |
| | value: |
| | formValues['passkey.user_verification'] || |
| | inputs['passkey.user_verification'] || |
| | 'preferred', |
| | }); |
| | options.push({ |
| | key: 'passkey.attachment_preference', |
| | value: |
| | formValues['passkey.attachment_preference'] || |
| | inputs['passkey.attachment_preference'] || |
| | '', |
| | }); |
| | options.push({ |
| | key: 'passkey.origins', |
| | value: formValues['passkey.origins'] || inputs['passkey.origins'] || '', |
| | }); |
| |
|
| | await updateOptions(options); |
| | }; |
| |
|
| | const handleCheckboxChange = async (optionKey, event) => { |
| | const value = event.target.checked; |
| |
|
| | if (optionKey === 'PasswordLoginEnabled' && !value) { |
| | setShowPasswordLoginConfirmModal(true); |
| | } else { |
| | await updateOptions([{ key: optionKey, value }]); |
| | } |
| | if (optionKey === 'LinuxDOOAuthEnabled') { |
| | setLinuxDOOAuthEnabled(value); |
| | } |
| | }; |
| |
|
| | const handlePasswordLoginConfirm = async () => { |
| | await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]); |
| | setShowPasswordLoginConfirmModal(false); |
| | }; |
| |
|
| | return ( |
| | <div> |
| | {isLoaded ? ( |
| | <Form |
| | initValues={inputs} |
| | onValueChange={handleFormChange} |
| | getFormApi={(api) => (formApiRef.current = api)} |
| | > |
| | {({ formState, values, formApi }) => ( |
| | <div |
| | style={{ |
| | display: 'flex', |
| | flexDirection: 'column', |
| | gap: '10px', |
| | marginTop: '10px', |
| | }} |
| | > |
| | <Card> |
| | <Form.Section text={t('通用设置')}> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Input |
| | field='ServerAddress' |
| | label={t('服务器地址')} |
| | placeholder='https://yourdomain.com' |
| | extraText={t( |
| | '该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置', |
| | )} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitServerAddress}> |
| | {t('更新服务器地址')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('代理设置')}> |
| | <Banner |
| | type='info' |
| | description={t( |
| | '此代理仅用于图片请求转发,Webhook通知发送等,AI API请求仍然由服务器直接发出,可在渠道设置中单独配置代理', |
| | )} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Text> |
| | {t('仅支持')}{' '} |
| | <a |
| | href='https://github.com/Calcium-Ion/new-api-worker' |
| | target='_blank' |
| | rel='noreferrer' |
| | > |
| | new-api-worker |
| | </a> |
| | {' '}{t('或其兼容new-api-worker格式的其他版本')} |
| | </Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='WorkerUrl' |
| | label={t('Worker地址')} |
| | placeholder='例如:https://workername.yourdomain.workers.dev' |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='WorkerValidKey' |
| | label={t('Worker密钥')} |
| | placeholder='敏感信息不会发送到前端显示' |
| | type='password' |
| | /> |
| | </Col> |
| | </Row> |
| | <Form.Checkbox |
| | field='WorkerAllowHttpImageRequestEnabled' |
| | noLabel |
| | > |
| | {t('允许 HTTP 协议图片请求(适用于自部署代理)')} |
| | </Form.Checkbox> |
| | <Button onClick={submitWorker}>{t('更新Worker设置')}</Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('SSRF防护设置')}> |
| | <Text extraText={t('SSRF防护详细说明')}> |
| | {t('配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全')} |
| | </Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Checkbox |
| | field='fetch_setting.enable_ssrf_protection' |
| | noLabel |
| | extraText={t('SSRF防护开关详细说明')} |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'fetch_setting.enable_ssrf_protection', |
| | e, |
| | ) |
| | } |
| | > |
| | {t('启用SSRF防护(推荐开启以保护服务器安全)')} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Checkbox |
| | field='fetch_setting.allow_private_ip' |
| | noLabel |
| | extraText={t('私有IP访问详细说明')} |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'fetch_setting.allow_private_ip', |
| | e, |
| | ) |
| | } |
| | > |
| | {t( |
| | '允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)', |
| | )} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Checkbox |
| | field='fetch_setting.apply_ip_filter_for_domain' |
| | noLabel |
| | extraText={t('域名IP过滤详细说明')} |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'fetch_setting.apply_ip_filter_for_domain', |
| | e, |
| | ) |
| | } |
| | style={{ marginBottom: 8 }} |
| | > |
| | {t('对域名启用 IP 过滤(实验性)')} |
| | </Form.Checkbox> |
| | <Text strong> |
| | {t(domainFilterMode ? '域名白名单' : '域名黑名单')} |
| | </Text> |
| | <Text |
| | type='secondary' |
| | style={{ display: 'block', marginBottom: 8 }} |
| | > |
| | {t( |
| | '支持通配符格式,如:example.com, *.api.example.com', |
| | )} |
| | </Text> |
| | <Radio.Group |
| | type='button' |
| | value={domainFilterMode ? 'whitelist' : 'blacklist'} |
| | onChange={(val) => { |
| | const selected = |
| | val && val.target ? val.target.value : val; |
| | const isWhitelist = selected === 'whitelist'; |
| | setDomainFilterMode(isWhitelist); |
| | setInputs((prev) => ({ |
| | ...prev, |
| | 'fetch_setting.domain_filter_mode': isWhitelist, |
| | })); |
| | }} |
| | style={{ marginBottom: 8 }} |
| | > |
| | <Radio value='whitelist'>{t('白名单')}</Radio> |
| | <Radio value='blacklist'>{t('黑名单')}</Radio> |
| | </Radio.Group> |
| | <TagInput |
| | value={domainList} |
| | onChange={(value) => { |
| | setDomainList(value); |
| | // 触发Form的onChange事件 |
| | setInputs((prev) => ({ |
| | ...prev, |
| | 'fetch_setting.domain_list': value, |
| | })); |
| | }} |
| | placeholder={t('输入域名后回车,如:example.com')} |
| | style={{ width: '100%' }} |
| | /> |
| | </Col> |
| | </Row> |
| | |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Text strong> |
| | {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')} |
| | </Text> |
| | <Text |
| | type='secondary' |
| | style={{ display: 'block', marginBottom: 8 }} |
| | > |
| | {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')} |
| | </Text> |
| | <Radio.Group |
| | type='button' |
| | value={ipFilterMode ? 'whitelist' : 'blacklist'} |
| | onChange={(val) => { |
| | const selected = |
| | val && val.target ? val.target.value : val; |
| | const isWhitelist = selected === 'whitelist'; |
| | setIpFilterMode(isWhitelist); |
| | setInputs((prev) => ({ |
| | ...prev, |
| | 'fetch_setting.ip_filter_mode': isWhitelist, |
| | })); |
| | }} |
| | style={{ marginBottom: 8 }} |
| | > |
| | <Radio value='whitelist'>{t('白名单')}</Radio> |
| | <Radio value='blacklist'>{t('黑名单')}</Radio> |
| | </Radio.Group> |
| | <TagInput |
| | value={ipList} |
| | onChange={(value) => { |
| | setIpList(value); |
| | // 触发Form的onChange事件 |
| | setInputs((prev) => ({ |
| | ...prev, |
| | 'fetch_setting.ip_list': value, |
| | })); |
| | }} |
| | placeholder={t('输入IP地址后回车,如:8.8.8.8')} |
| | style={{ width: '100%' }} |
| | /> |
| | </Col> |
| | </Row> |
| | |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Text strong>{t('允许的端口')}</Text> |
| | <Text |
| | type='secondary' |
| | style={{ display: 'block', marginBottom: 8 }} |
| | > |
| | {t('支持单个端口和端口范围,如:80, 443, 8000-8999')} |
| | </Text> |
| | <TagInput |
| | value={allowedPorts} |
| | onChange={(value) => { |
| | setAllowedPorts(value); |
| | // 触发Form的onChange事件 |
| | setInputs((prev) => ({ |
| | ...prev, |
| | 'fetch_setting.allowed_ports': value, |
| | })); |
| | }} |
| | placeholder={t('输入端口后回车,如:80 或 8000-8999')} |
| | style={{ width: '100%' }} |
| | /> |
| | <Text |
| | type='secondary' |
| | style={{ display: 'block', marginBottom: 8 }} |
| | > |
| | {t('端口配置详细说明')} |
| | </Text> |
| | </Col> |
| | </Row> |
| | |
| | <Button onClick={submitSSRF} style={{ marginTop: 16 }}> |
| | {t('更新SSRF防护设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置登录注册')}> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Checkbox |
| | field='PasswordLoginEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('PasswordLoginEnabled', e) |
| | } |
| | > |
| | {t('允许通过密码进行登录')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='PasswordRegisterEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('PasswordRegisterEnabled', e) |
| | } |
| | > |
| | {t('允许通过密码进行注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='EmailVerificationEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('EmailVerificationEnabled', e) |
| | } |
| | > |
| | {t('通过密码注册时需要进行邮箱验证')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='RegisterEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('RegisterEnabled', e) |
| | } |
| | > |
| | {t('允许新用户注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='TurnstileCheckEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('TurnstileCheckEnabled', e) |
| | } |
| | > |
| | {t('允许 Turnstile 用户校验')} |
| | </Form.Checkbox> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Checkbox |
| | field='GitHubOAuthEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('GitHubOAuthEnabled', e) |
| | } |
| | > |
| | {t('允许通过 GitHub 账户登录 & 注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='discord.enabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('discord.enabled', e) |
| | } |
| | > |
| | {t('允许通过 Discord 账户登录 & 注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='LinuxDOOAuthEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('LinuxDOOAuthEnabled', e) |
| | } |
| | > |
| | {t('允许通过 Linux DO 账户登录 & 注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='WeChatAuthEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('WeChatAuthEnabled', e) |
| | } |
| | > |
| | {t('允许通过微信登录 & 注册')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field='TelegramOAuthEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('TelegramOAuthEnabled', e) |
| | } |
| | > |
| | {t('允许通过 Telegram 进行登录')} |
| | </Form.Checkbox> |
| | <Form.Checkbox |
| | field="['oidc.enabled']" |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('oidc.enabled', e) |
| | } |
| | > |
| | {t('允许通过 OIDC 进行登录')} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置 Passkey')}> |
| | <Text>{t('用以支持基于 WebAuthn 的无密码登录注册')}</Text> |
| | <Banner |
| | type='info' |
| | description={t( |
| | 'Passkey 是基于 WebAuthn 标准的无密码身份验证方法,支持指纹、面容、硬件密钥等认证方式', |
| | )} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Checkbox |
| | field="['passkey.enabled']" |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('passkey.enabled', e) |
| | } |
| | > |
| | {t('允许通过 Passkey 登录 & 认证')} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['passkey.rp_display_name']" |
| | label={t('服务显示名称')} |
| | placeholder={t('默认使用系统名称')} |
| | extraText={t( |
| | "用户注册时看到的网站名称,比如'我的网站'", |
| | )} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['passkey.rp_id']" |
| | label={t('网站域名标识')} |
| | placeholder={t('例如:example.com')} |
| | extraText={t( |
| | '留空则默认使用服务器地址,注意不能携带http://或者https://', |
| | )} |
| | /> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Select |
| | field="['passkey.user_verification']" |
| | label={t('安全验证级别')} |
| | placeholder={t('是否要求指纹/面容等生物识别')} |
| | optionList={[ |
| | { |
| | label: t('推荐使用(用户可选)'), |
| | value: 'preferred', |
| | }, |
| | { label: t('强制要求'), value: 'required' }, |
| | { label: t('不建议使用'), value: 'discouraged' }, |
| | ]} |
| | extraText={t('推荐:用户可以选择是否使用指纹等验证')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Select |
| | field="['passkey.attachment_preference']" |
| | label={t('设备类型偏好')} |
| | placeholder={t('选择支持的认证设备类型')} |
| | optionList={[ |
| | { label: t('不限制'), value: '' }, |
| | { label: t('本设备内置'), value: 'platform' }, |
| | { label: t('外接设备'), value: 'cross-platform' }, |
| | ]} |
| | extraText={t( |
| | '本设备:手机指纹/面容,外接:USB安全密钥', |
| | )} |
| | /> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Checkbox |
| | field="['passkey.allow_insecure_origin']" |
| | noLabel |
| | extraText={t('仅用于开发环境,生产环境应使用 HTTPS')} |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'passkey.allow_insecure_origin', |
| | e, |
| | ) |
| | } |
| | > |
| | {t('允许不安全的 Origin(HTTP)')} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={24} lg={24} xl={24}> |
| | <Form.Input |
| | field="['passkey.origins']" |
| | label={t('允许的 Origins')} |
| | placeholder={t('填写带https的域名,逗号分隔')} |
| | extraText={t( |
| | '为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https', |
| | )} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button |
| | onClick={submitPasskeySettings} |
| | style={{ marginTop: 16 }} |
| | > |
| | {t('保存 Passkey 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置邮箱域名白名单')}> |
| | <Text>{t('用以防止恶意用户利用临时邮箱批量注册')}</Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Checkbox |
| | field='EmailDomainRestrictionEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'EmailDomainRestrictionEnabled', |
| | e, |
| | ) |
| | } |
| | > |
| | 启用邮箱域名白名单 |
| | </Form.Checkbox> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Checkbox |
| | field='EmailAliasRestrictionEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange( |
| | 'EmailAliasRestrictionEnabled', |
| | e, |
| | ) |
| | } |
| | > |
| | 启用邮箱别名限制 |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | <TagInput |
| | value={emailDomainWhitelist} |
| | onChange={setEmailDomainWhitelist} |
| | placeholder={t('输入域名后回车')} |
| | style={{ width: '100%', marginTop: 16 }} |
| | /> |
| | <Form.Input |
| | placeholder={t('输入要添加的邮箱域名')} |
| | value={emailToAdd} |
| | onChange={(value) => setEmailToAdd(value)} |
| | style={{ marginTop: 16 }} |
| | suffix={ |
| | <Button |
| | theme='solid' |
| | type='primary' |
| | onClick={handleAddEmail} |
| | > |
| | {t('添加')} |
| | </Button> |
| | } |
| | onEnterPress={handleAddEmail} |
| | /> |
| | <Button |
| | onClick={submitEmailDomainWhitelist} |
| | style={{ marginTop: 10 }} |
| | > |
| | {t('保存邮箱域名白名单设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | <Card> |
| | <Form.Section text={t('配置 SMTP')}> |
| | <Text>{t('用以支持系统的邮件发送')}</Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='SMTPServer' |
| | label={t('SMTP 服务器地址')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input field='SMTPPort' label={t('SMTP 端口')} /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input field='SMTPAccount' label={t('SMTP 账户')} /> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | style={{ marginTop: 16 }} |
| | > |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='SMTPFrom' |
| | label={t('SMTP 发送者邮箱')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='SMTPToken' |
| | label={t('SMTP 访问凭证')} |
| | type='password' |
| | placeholder='敏感信息不会发送到前端显示' |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Checkbox |
| | field='SMTPSSLEnabled' |
| | noLabel |
| | onChange={(e) => |
| | handleCheckboxChange('SMTPSSLEnabled', e) |
| | } |
| | > |
| | {t('启用SMTP SSL')} |
| | </Form.Checkbox> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitSMTP}>{t('保存 SMTP 设置')}</Button> |
| | </Form.Section> |
| | </Card> |
| | <Card> |
| | <Form.Section text={t('配置 OIDC')}> |
| | <Text> |
| | {t( |
| | '用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP', |
| | )} |
| | </Text> |
| | <Banner |
| | type='info' |
| | description={`${t('主页链接填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('重定向 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/oidc`} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Text> |
| | {t( |
| | '若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置', |
| | )} |
| | </Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.well_known']" |
| | label={t('Well-Known URL')} |
| | placeholder={t('请输入 OIDC 的 Well-Known URL')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.client_id']" |
| | label={t('Client ID')} |
| | placeholder={t('输入 OIDC 的 Client ID')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.client_secret']" |
| | label={t('Client Secret')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.authorization_endpoint']" |
| | label={t('Authorization Endpoint')} |
| | placeholder={t('输入 OIDC 的 Authorization Endpoint')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.token_endpoint']" |
| | label={t('Token Endpoint')} |
| | placeholder={t('输入 OIDC 的 Token Endpoint')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['oidc.user_info_endpoint']" |
| | label={t('User Info Endpoint')} |
| | placeholder={t('输入 OIDC 的 Userinfo Endpoint')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitOIDCSettings}> |
| | {t('保存 OIDC 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置 GitHub OAuth App')}> |
| | <Text>{t('用以支持通过 GitHub 进行登录注册')}</Text> |
| | <Banner |
| | type='info' |
| | description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/github`} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='GitHubClientId' |
| | label={t('GitHub Client ID')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='GitHubClientSecret' |
| | label={t('GitHub Client Secret')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitGitHubOAuth}> |
| | {t('保存 GitHub OAuth 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | <Card> |
| | <Form.Section text={t('配置 Discord OAuth')}> |
| | <Text>{t('用以支持通过 Discord 进行登录注册')}</Text> |
| | <Banner |
| | type='info' |
| | description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/discord`} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['discord.client_id']" |
| | label={t('Discord Client ID')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field="['discord.client_secret']" |
| | label={t('Discord Client Secret')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitDiscordOAuth}> |
| | {t('保存 Discord OAuth 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | <Card> |
| | <Form.Section text={t('配置 Linux DO OAuth')}> |
| | <Text> |
| | {t('用以支持通过 Linux DO 进行登录注册')} |
| | <a |
| | href='https://connect.linux.do/' |
| | target='_blank' |
| | rel='noreferrer' |
| | style={{ |
| | display: 'inline-block', |
| | marginLeft: 4, |
| | marginRight: 4, |
| | }} |
| | > |
| | {t('点击此处')} |
| | </a> |
| | {t('管理你的 LinuxDO OAuth App')} |
| | </Text> |
| | <Banner |
| | type='info' |
| | description={`${t('回调 URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/linuxdo`} |
| | style={{ marginBottom: 20, marginTop: 16 }} |
| | /> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={10} lg={10} xl={10}> |
| | <Form.Input |
| | field='LinuxDOClientId' |
| | label={t('Linux DO Client ID')} |
| | placeholder={t('输入你注册的 LinuxDO OAuth APP 的 ID')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={10} lg={10} xl={10}> |
| | <Form.Input |
| | field='LinuxDOClientSecret' |
| | label={t('Linux DO Client Secret')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={4} lg={4} xl={4}> |
| | <Form.Input |
| | field='LinuxDOMinimumTrustLevel' |
| | label='LinuxDO Minimum Trust Level' |
| | placeholder='允许注册的最低信任等级' |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitLinuxDOOAuth}> |
| | {t('保存 Linux DO OAuth 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置 WeChat Server')}> |
| | <Text>{t('用以支持通过微信进行登录注册')}</Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='WeChatServerAddress' |
| | label={t('WeChat Server 服务器地址')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='WeChatServerToken' |
| | label={t('WeChat Server 访问凭证')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={8} lg={8} xl={8}> |
| | <Form.Input |
| | field='WeChatAccountQRCodeImageURL' |
| | label={t('微信公众号二维码图片链接')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitWeChat}> |
| | {t('保存 WeChat Server 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置 Telegram 登录')}> |
| | <Text>{t('用以支持通过 Telegram 进行登录注册')}</Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='TelegramBotToken' |
| | label={t('Telegram Bot Token')} |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | type='password' |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='TelegramBotName' |
| | label={t('Telegram Bot 名称')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitTelegramSettings}> |
| | {t('保存 Telegram 登录设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Card> |
| | <Form.Section text={t('配置 Turnstile')}> |
| | <Text>{t('用以支持用户校验')}</Text> |
| | <Row |
| | gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }} |
| | > |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='TurnstileSiteKey' |
| | label={t('Turnstile Site Key')} |
| | /> |
| | </Col> |
| | <Col xs={24} sm={24} md={12} lg={12} xl={12}> |
| | <Form.Input |
| | field='TurnstileSecretKey' |
| | label={t('Turnstile Secret Key')} |
| | type='password' |
| | placeholder={t('敏感信息不会发送到前端显示')} |
| | /> |
| | </Col> |
| | </Row> |
| | <Button onClick={submitTurnstile}> |
| | {t('保存 Turnstile 设置')} |
| | </Button> |
| | </Form.Section> |
| | </Card> |
| | |
| | <Modal |
| | title={t('确认取消密码登录')} |
| | visible={showPasswordLoginConfirmModal} |
| | onOk={handlePasswordLoginConfirm} |
| | onCancel={() => { |
| | setShowPasswordLoginConfirmModal(false); |
| | formApiRef.current.setValue('PasswordLoginEnabled', true); |
| | }} |
| | okText={t('确认')} |
| | cancelText={t('取消')} |
| | > |
| | <p> |
| | {t( |
| | '您确定要取消密码登录功能吗?这可能会影响用户的登录方式。', |
| | )} |
| | </p> |
| | </Modal> |
| | </div> |
| | )} |
| | </Form> |
| | ) : ( |
| | <div |
| | style={{ |
| | display: 'flex', |
| | justifyContent: 'center', |
| | alignItems: 'center', |
| | height: '100vh', |
| | }} |
| | > |
| | <Spin size='large' /> |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default SystemSetting; |
| |
|