|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|