/* Copyright (c) 2025 Tethys Plex This file is part of Veloera. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ import React, { useEffect, useState, useRef } from 'react'; import { Button, Form, Row, Col, Typography, Modal, Banner, TagInput, Spin, Card, Select, } from '@douyinfe/semi-ui'; const { Text } = Typography; import { removeTrailingSlash, showError, showSuccess, verifyJSON, } from '../helpers/utils'; import { API } from '../helpers/api'; const SystemSetting = () => { let [inputs, setInputs] = useState({ PasswordLoginEnabled: '', PasswordRegisterEnabled: '', EmailVerificationEnabled: '', GitHubOAuthEnabled: '', GitHubClientId: '', GitHubClientSecret: '', '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: '', ServerAddress: '', WorkerUrl: '', WorkerValidKey: '', EpayId: '', EpayKey: '', Price: 7.3, MinTopUp: 1, TopupGroupRatio: '', PayAddress: '', CustomCallbackAddress: '', Footer: '', WeChatAuthEnabled: '', WeChatServerAddress: '', WeChatServerToken: '', WeChatAccountQRCodeImageURL: '', TurnstileCheckEnabled: '', TurnstileSiteKey: '', TurnstileSecretKey: '', RegisterEnabled: '', EmailDomainRestrictionEnabled: '', EmailAliasRestrictionEnabled: '', SMTPSSLEnabled: '', EmailDomainWhitelist: [], // telegram login TelegramOAuthEnabled: '', TelegramBotToken: '', TelegramBotName: '', LinuxDOOAuthEnabled: '', LinuxDOClientId: '', LinuxDOClientSecret: '', LinuxDOMinimumTrustLevel: '', IDCFlareOAuthEnabled: '', IDCFlareClientId: '', IDCFlareClientSecret: '', IDCFlareMinimumTrustLevel: '', // reverse proxy settings ReverseProxyEnabled: '', ReverseProxyProvider: '', }); 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 [idcFlareOAuthEnabled, setIDCFlareOAuthEnabled] = useState(false); const [emailToAdd, setEmailToAdd] = 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 'PasswordLoginEnabled': case 'PasswordRegisterEnabled': case 'EmailVerificationEnabled': case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': case 'TelegramOAuthEnabled': case 'RegisterEnabled': case 'TurnstileCheckEnabled': case 'EmailDomainRestrictionEnabled': case 'EmailAliasRestrictionEnabled': case 'SMTPSSLEnabled': case 'LinuxDOOAuthEnabled': case 'IDCFlareOAuthEnabled': case 'ReverseProxyEnabled': case 'oidc.enabled': item.value = item.value === 'true'; break; case 'Price': case 'MinTopUp': item.value = parseFloat(item.value); break; default: break; } newInputs[item.key] = item.value; }); setInputs(newInputs); setOriginInputs(newInputs); if (formApiRef.current) { formApiRef.current.setValues(newInputs); } setIsLoaded(true); } else { showError(message); } setLoading(false); }; useEffect(() => { getOptions(); }, []); const updateOptions = async (options) => { setLoading(true); try { // 分离 checkbox 类型的选项和其他选项 const checkboxOptions = options.filter((opt) => opt.key.toLowerCase().endsWith('enabled'), ); const otherOptions = options.filter( (opt) => !opt.key.toLowerCase().endsWith('enabled'), ); // 处理 checkbox 类型的选项 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('更新成功'); // 更新本地状态 const newInputs = { ...inputs }; options.forEach((opt) => { newInputs[opt.key] = opt.value; }); setInputs(newInputs); } catch (error) { showError('更新失败'); } setLoading(false); }; const handleFormChange = (values) => { setInputs(values); }; const submitServerAddress = async () => { let ServerAddress = removeTrailingSlash(inputs.ServerAddress); await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]); }; const submitWorker = async () => { let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl); await updateOptions([ { key: 'WorkerUrl', value: WorkerUrl }, { key: 'WorkerValidKey', value: inputs.WorkerValidKey }, ]); }; const submitPayAddress = async () => { if (inputs.ServerAddress === '') { showError('请先填写服务器地址'); return; } if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { if (!verifyJSON(inputs.TopupGroupRatio)) { showError('充值分组倍率不是合法的 JSON 字符串'); return; } } const options = [ { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }, ]; if (inputs.EpayId !== '') { options.push({ key: 'EpayId', value: inputs.EpayId }); } if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') { options.push({ key: 'EpayKey', value: inputs.EpayKey }); } if (inputs.Price !== '') { options.push({ key: 'Price', value: inputs.Price.toString() }); } if (inputs.MinTopUp !== '') { options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() }); } if (inputs.CustomCallbackAddress !== '') { options.push({ key: 'CustomCallbackAddress', value: inputs.CustomCallbackAddress, }); } if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio }); } await updateOptions(options); }; 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('邮箱域名白名单格式不正确'); } }; 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('邮箱域名格式不正确,请输入有效的域名,如 gmail.com'); return; } // 检查是否已存在 if (emailDomainWhitelist.includes(domain)) { showError('该域名已存在于白名单中'); return; } setEmailDomainWhitelist([...emailDomainWhitelist, domain]); setEmailToAdd(''); showSuccess('已添加到白名单'); } }; 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 submitOIDCSettings = async () => { if (inputs['oidc.well_known'] !== '') { if ( !inputs['oidc.well_known'].startsWith('http://') && !inputs['oidc.well_known'].startsWith('https://') ) { showError('Well-Known URL 必须以 http:// 或 https:// 开头'); return; } try { const res = await API.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('获取 OIDC 配置成功!'); } catch (err) { console.error(err); showError( '获取 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.toString(), }); } if (options.length > 0) { await updateOptions(options); } }; const submitIDCFlareOAuth = async () => { const options = []; if (originInputs['IDCFlareClientId'] !== inputs.IDCFlareClientId) { options.push({ key: 'IDCFlareClientId', value: inputs.IDCFlareClientId }); } if ( originInputs['IDCFlareClientSecret'] !== inputs.IDCFlareClientSecret && inputs.IDCFlareClientSecret !== '' ) { options.push({ key: 'IDCFlareClientSecret', value: inputs.IDCFlareClientSecret, }); } if (originInputs['IDCFlareMinimumTrustLevel'] !== inputs.IDCFlareMinimumTrustLevel) { options.push({ key: 'IDCFlareMinimumTrustLevel', value: inputs.IDCFlareMinimumTrustLevel.toString(), }); } if (options.length > 0) { await updateOptions(options); } }; const submitReverseProxy = async () => { const options = []; if (originInputs['ReverseProxyEnabled'] !== inputs.ReverseProxyEnabled) { options.push({ key: 'ReverseProxyEnabled', value: inputs.ReverseProxyEnabled }); } if (originInputs['ReverseProxyProvider'] !== inputs.ReverseProxyProvider) { options.push({ key: 'ReverseProxyProvider', value: inputs.ReverseProxyProvider }); } if (options.length > 0) { 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); } if (optionKey === 'IDCFlareOAuthEnabled') { setIDCFlareOAuthEnabled(value); } }; const handlePasswordLoginConfirm = async () => { await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]); setShowPasswordLoginConfirmModal(false); }; return (
{isLoaded ? (
(formApiRef.current = api)} > {({ formState, values, formApi }) => (
用以支持系统在反向代理后运行时正确识别客户端IP地址 handleCheckboxChange('ReverseProxyEnabled', e) } > 系统在反向代理后运行 {inputs.ReverseProxyEnabled && ( Nginx / OpenResty (通用) Cloudflare )} (支持{' '} new-api-worker (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!) handleCheckboxChange('PasswordLoginEnabled', e) } > 允许通过密码进行登录 handleCheckboxChange('PasswordRegisterEnabled', e) } > 允许通过密码进行注册 handleCheckboxChange('EmailVerificationEnabled', e) } > 通过密码注册时需要进行邮箱验证 handleCheckboxChange('RegisterEnabled', e) } > 允许新用户注册 handleCheckboxChange('TurnstileCheckEnabled', e) } > 启用 Turnstile 用户校验 handleCheckboxChange('GitHubOAuthEnabled', e) } > 允许通过 GitHub 账户登录 & 注册 handleCheckboxChange('LinuxDOOAuthEnabled', e) } > 允许通过 Linux DO 账户登录 & 注册 handleCheckboxChange('IDCFlareOAuthEnabled', e) } > 允许通过 IDC Flare 账户登录 & 注册 handleCheckboxChange('WeChatAuthEnabled', e) } > 允许通过微信登录 & 注册 handleCheckboxChange('TelegramOAuthEnabled', e) } > 允许通过 Telegram 进行登录 handleCheckboxChange('oidc.enabled', e) } > 允许通过 OIDC 进行登录 用以防止恶意用户利用临时邮箱批量注册 handleCheckboxChange( 'EmailDomainRestrictionEnabled', e, ) } > 启用邮箱域名白名单 handleCheckboxChange( 'EmailAliasRestrictionEnabled', e, ) } > 启用邮箱别名限制 setEmailToAdd(value)} style={{ marginTop: 16 }} suffix={ } onEnterPress={handleAddEmail} /> 用以支持系统的邮件发送 handleCheckboxChange('SMTPSSLEnabled', e) } > 启用SMTP SSL 用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP 若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置 用以支持通过 GitHub 进行登录注册 用以支持通过 Linux DO 进行登录注册 点击此处 管理你的 LinuxDO OAuth App 用以支持通过 IDC Flare 进行登录注册 点击此处 管理你的 IDC Flare OAuth App 用以支持通过微信进行登录注册 用以支持通过 Telegram 进行登录注册 用以支持用户校验 { setShowPasswordLoginConfirmModal(false); formApiRef.current.setValue('PasswordLoginEnabled', true); }} okText='确认' cancelText='取消' >

您确定要取消密码登录功能吗?这可能会影响用户的登录方式。

)}
) : (
)}
); }; export default SystemSetting;