This commit is contained in:
ryan
2026-05-13 10:53:14 +08:00
parent 856e3f46d2
commit e85df49962
31 changed files with 2504 additions and 645 deletions
@@ -0,0 +1,441 @@
'use client';
import { useEffect, useState } from 'react';
import { ErrorState } from '@/components/feedback/error-state';
import { InlineMessage } from '@/components/feedback/inline-message';
import { LoadingState } from '@/components/feedback/loading-state';
import { AppModal } from '@/components/ui/app-modal';
import {
createAuthSource,
deleteAuthSource,
toggleAuthSource,
updateAuthSource,
} from '@/features/settings/api/settings';
import type {
AuthSource,
AuthSourcePayload,
AuthSourceType,
} from '@/features/settings/types';
import {
DangerButton,
PrimaryButton,
ResourceField,
ResourceInput,
ResourceSelect,
SecondaryButton,
} from '@/features/shared/components/resource-primitives';
const emptyForm: AuthSourcePayload = {
name: '',
type: 'github',
display_name: '',
is_active: false,
client_id: '',
client_secret: '',
openid_discovery_url: '',
scopes: 'user:email',
icon_url: '',
};
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
}
function sourceToForm(source: AuthSource): AuthSourcePayload {
return {
id: source.id,
name: source.name,
type: source.type,
display_name: source.display_name,
is_active: source.is_active,
client_id: source.client_id,
client_secret: '',
openid_discovery_url: source.openid_discovery_url,
scopes:
source.scopes ||
(source.type === 'oidc' ? 'openid profile email' : 'user:email'),
icon_url: source.icon_url,
};
}
function buildCallbackURL(origin: string, sourceName: string) {
const normalizedName = sourceName.trim() || '认证源名称';
return `${origin || '当前访问地址'}/oauth/${encodeURIComponent(normalizedName)}`;
}
export function AuthSourceModal({
isOpen,
sources,
isLoading,
error,
onClose,
onChanged,
}: {
isOpen: boolean;
sources: AuthSource[];
isLoading: boolean;
error: unknown;
onClose: () => void;
onChanged: () => Promise<void>;
}) {
const [mode, setMode] = useState<'list' | 'edit'>('list');
const [editingSource, setEditingSource] = useState<AuthSource | null>(null);
const [form, setForm] = useState<AuthSourcePayload>(emptyForm);
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState<{
tone: 'success' | 'danger' | 'info';
text: string;
} | null>(null);
const [browserOrigin, setBrowserOrigin] = useState('');
useEffect(() => {
if (!isOpen) {
setMode('list');
setEditingSource(null);
setForm(emptyForm);
setBusyKey(null);
setMessage(null);
}
}, [isOpen]);
useEffect(() => {
if (typeof window !== 'undefined') {
setBrowserOrigin(window.location.origin);
}
}, []);
const startCreate = () => {
setEditingSource(null);
setForm(emptyForm);
setMessage(null);
setMode('edit');
};
const startEdit = (source: AuthSource) => {
setEditingSource(source);
setForm(sourceToForm(source));
setMessage(null);
setMode('edit');
};
const updateType = (type: AuthSourceType) => {
setForm((previous) => ({
...previous,
type,
scopes:
previous.scopes === 'user:email' ||
previous.scopes === 'openid profile email' ||
!previous.scopes
? type === 'oidc'
? 'openid profile email'
: 'user:email'
: previous.scopes,
}));
};
const runAction = async (key: string, action: () => Promise<void>) => {
setBusyKey(key);
setMessage(null);
try {
await action();
} catch (actionError) {
setMessage({ tone: 'danger', text: getErrorMessage(actionError) });
} finally {
setBusyKey(null);
}
};
const saveForm = () => {
void runAction('save', async () => {
const payload: AuthSourcePayload = {
...form,
name: form.name.trim(),
display_name: form.display_name.trim(),
client_id: form.client_id.trim(),
client_secret: form.client_secret.trim(),
openid_discovery_url: form.openid_discovery_url.trim(),
scopes: form.scopes.trim(),
icon_url: form.icon_url.trim(),
};
if (editingSource) {
await updateAuthSource(editingSource.id, payload);
} else {
await createAuthSource(payload);
}
await onChanged();
setMessage({ tone: 'success', text: '认证源已保存。' });
setMode('list');
setEditingSource(null);
setForm(emptyForm);
});
};
const removeSource = (source: AuthSource) => {
if (
!window.confirm(
`确定删除认证源「${source.display_name || source.name}」吗?`,
)
) {
return;
}
void runAction(`delete-${source.id}`, async () => {
await deleteAuthSource(source.id);
await onChanged();
setMessage({ tone: 'success', text: '认证源已删除。' });
});
};
const toggleSource = (source: AuthSource) => {
void runAction(`toggle-${source.id}`, async () => {
await toggleAuthSource(source.id, !source.is_active);
await onChanged();
setMessage({ tone: 'success', text: '认证源状态已更新。' });
});
};
return (
<AppModal
isOpen={isOpen}
title="认证源"
description="配置 GitHub 或标准 OIDC 登录入口。启用后会显示在登录页。"
size="xl"
onClose={onClose}
footer={
mode === 'edit' ? (
<div className="flex justify-end gap-3">
<SecondaryButton type="button" onClick={() => setMode('list')}>
返回列表
</SecondaryButton>
<PrimaryButton
type="button"
onClick={saveForm}
disabled={busyKey === 'save'}
>
{busyKey === 'save' ? '保存中...' : '保存认证源'}
</PrimaryButton>
</div>
) : (
<div className="flex justify-end">
<SecondaryButton type="button" onClick={onClose}>
关闭
</SecondaryButton>
</div>
)
}
>
<div className="space-y-5">
{message ? (
<InlineMessage tone={message.tone} message={message.text} />
) : null}
{mode === 'list' ? (
<div className="space-y-4">
<div className="flex justify-end">
<PrimaryButton type="button" onClick={startCreate}>
新增认证源
</PrimaryButton>
</div>
{isLoading ? <LoadingState /> : null}
{error ? (
<ErrorState
title="认证源加载失败"
description={getErrorMessage(error)}
/>
) : null}
{!isLoading && !error && sources.length === 0 ? (
<div className="rounded-2xl border border-dashed border-[var(--border-default)] px-5 py-8 text-center text-sm text-[var(--foreground-secondary)]">
暂无认证源。
</div>
) : null}
{!isLoading && !error && sources.length > 0 ? (
<div className="overflow-hidden rounded-2xl border border-[var(--border-default)]">
<table className="w-full min-w-[720px] text-left text-sm">
<thead className="bg-[var(--surface-elevated)] text-xs text-[var(--foreground-secondary)] uppercase">
<tr>
<th className="px-4 py-3 font-medium">名称</th>
<th className="px-4 py-3 font-medium">类型</th>
<th className="px-4 py-3 font-medium">状态</th>
<th className="px-4 py-3 font-medium">Client ID</th>
<th className="px-4 py-3 text-right font-medium">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--border-default)]">
{sources.map((source) => (
<tr key={source.id}>
<td className="px-4 py-3">
<div className="font-medium text-[var(--foreground-primary)]">
{source.display_name || source.name}
</div>
<div className="text-xs text-[var(--foreground-secondary)]">
{source.name}
</div>
</td>
<td className="px-4 py-3 uppercase">{source.type}</td>
<td className="px-4 py-3">
{source.is_active ? '已启用' : '已禁用'}
</td>
<td className="max-w-[220px] truncate px-4 py-3">
{source.client_id || '-'}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap justify-end gap-2">
<SecondaryButton
type="button"
onClick={() => toggleSource(source)}
disabled={busyKey === `toggle-${source.id}`}
>
{source.is_active ? '禁用' : '启用'}
</SecondaryButton>
<SecondaryButton
type="button"
onClick={() => startEdit(source)}
>
修改
</SecondaryButton>
<DangerButton
type="button"
onClick={() => removeSource(source)}
disabled={busyKey === `delete-${source.id}`}
>
删除
</DangerButton>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
) : (
<div className="grid gap-5 md:grid-cols-2">
<ResourceField label="认证源名称">
<ResourceInput
value={form.name}
onChange={(event) =>
setForm((previous) => ({
...previous,
name: event.target.value,
}))
}
placeholder="GitHub"
/>
</ResourceField>
<ResourceField label="展示名称">
<ResourceInput
value={form.display_name}
onChange={(event) =>
setForm((previous) => ({
...previous,
display_name: event.target.value,
}))
}
placeholder="GitHub"
/>
</ResourceField>
<ResourceField label="类型">
<ResourceSelect
value={form.type}
onChange={(event) =>
updateType(event.target.value as AuthSourceType)
}
>
<option value="github">GitHub</option>
<option value="oidc">OIDC</option>
</ResourceSelect>
</ResourceField>
<ResourceField label="状态">
<ResourceSelect
value={form.is_active ? 'true' : 'false'}
onChange={(event) =>
setForm((previous) => ({
...previous,
is_active: event.target.value === 'true',
}))
}
>
<option value="false">禁用</option>
<option value="true">启用</option>
</ResourceSelect>
</ResourceField>
<ResourceField label="Client ID">
<ResourceInput
value={form.client_id}
onChange={(event) =>
setForm((previous) => ({
...previous,
client_id: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField
label="Client Secret"
hint={editingSource ? '留空表示不更新现有密钥。' : undefined}
>
<ResourceInput
type="password"
value={form.client_secret}
onChange={(event) =>
setForm((previous) => ({
...previous,
client_secret: event.target.value,
}))
}
/>
</ResourceField>
<div className="md:col-span-2 rounded-2xl border border-[var(--status-info-border)] bg-[var(--status-info-soft)] px-4 py-3 text-sm leading-6 text-[var(--status-info-foreground)]">
第三方平台的 Redirect URI / Callback URL 请填写:
<span className="font-medium">
{' '}
{buildCallbackURL(browserOrigin, form.name)}
</span>
。末尾路径使用上方填写的认证源名称,保存后如修改认证源名称,也需要同步更新第三方平台中的回调地址。
</div>
{form.type === 'oidc' ? (
<ResourceField
label="OIDC Discovery URL"
className="md:col-span-2"
>
<ResourceInput
value={form.openid_discovery_url}
onChange={(event) =>
setForm((previous) => ({
...previous,
openid_discovery_url: event.target.value,
}))
}
placeholder="https://auth.example.com/.well-known/openid-configuration"
/>
</ResourceField>
) : null}
<ResourceField label="Scopes">
<ResourceInput
value={form.scopes}
onChange={(event) =>
setForm((previous) => ({
...previous,
scopes: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="图标 URL">
<ResourceInput
value={form.icon_url}
onChange={(event) =>
setForm((previous) => ({
...previous,
icon_url: event.target.value,
}))
}
/>
</ResourceField>
</div>
)}
</div>
</AppModal>
);
}
@@ -13,13 +13,16 @@ import { useAuth } from '@/components/providers/auth-provider';
import { PageHeader } from '@/components/layout/page-header';
import { AppCard } from '@/components/ui/app-card';
import { StatusBadge } from '@/components/ui/status-badge';
import { sendEmailVerification } from '@/features/auth/api/auth';
import {
getOAuthAuthorizeUrl,
sendEmailVerification,
} from '@/features/auth/api/auth';
import { getPublicStatus } from '@/features/auth/api/public';
import {
bindEmail,
bindWeChat,
cleanupDatabaseObservability,
generateAccessToken,
getAuthSources,
getBootstrapToken,
getOptions,
getSettingsProfile,
@@ -28,6 +31,7 @@ import {
updateOptions,
updateSelf,
} from '@/features/settings/api/settings';
import { AuthSourceModal } from '@/features/settings/components/auth-source-modal';
import type {
BootstrapTokenPayload,
DatabaseCleanupResult,
@@ -50,6 +54,7 @@ import {
import { formatDateTime } from '@/lib/utils/date';
const settingsQueryKey = ['settings', 'options'] as const;
const authSourcesQueryKey = ['settings', 'auth-sources'] as const;
const installerScriptUrl =
'https://raw.githubusercontent.com/Rain-kl/OpenFlare/main/scripts/install-agent.sh';
@@ -152,12 +157,7 @@ type CleanupModalState = {
label: string;
};
type SettingsTab =
| 'personal'
| 'operation'
| 'database'
| 'system'
| 'other';
type SettingsTab = 'personal' | 'operation' | 'database' | 'system' | 'other';
function getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : '请求失败,请稍后重试。';
@@ -249,10 +249,10 @@ export function SettingsPage() {
const [otherFields, setOtherFields] = useState(defaultOtherFields);
const [databaseFields, setDatabaseFields] = useState(defaultDatabaseFields);
const [accessToken, setAccessToken] = useState('');
const [wechatCode, setWeChatCode] = useState('');
const [emailAddress, setEmailAddress] = useState('');
const [emailCode, setEmailCode] = useState('');
const [emailTurnstileToken, setEmailTurnstileToken] = useState('');
const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false);
const [geoIPTestIP, setGeoIPTestIP] = useState('8.8.8.8');
const [cleanupModalState, setCleanupModalState] =
useState<CleanupModalState | null>(null);
@@ -276,6 +276,12 @@ export function SettingsPage() {
enabled: isRoot,
});
const authSourcesQuery = useQuery({
queryKey: authSourcesQueryKey,
queryFn: getAuthSources,
enabled: isRoot,
});
const bootstrapQuery = useQuery({
queryKey: ['settings', 'bootstrap-token'],
queryFn: getBootstrapToken,
@@ -523,7 +529,7 @@ export function SettingsPage() {
{
key: 'system' as const,
label: '系统设置',
description: '登录注册、SMTP、OAuth、限流与风控开关。',
description: '登录注册、SMTP、认证源、限流与风控开关。',
},
{
key: 'database' as const,
@@ -629,20 +635,10 @@ export function SettingsPage() {
});
};
const handleBindWeChat = () => {
if (!wechatCode.trim()) {
setFeedback({ tone: 'danger', message: '请输入微信验证码。' });
return;
}
void runBusyAction('wechat-bind', async () => {
await bindWeChat(wechatCode.trim());
setWeChatCode('');
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['settings', 'profile'] }),
refreshUser(),
]);
setFeedback({ tone: 'success', message: '微信账号已绑定。' });
const handleBindAuthSource = (sourceName: string) => {
void runBusyAction(`auth-source-bind-${sourceName}`, async () => {
const result = await getOAuthAuthorizeUrl(sourceName);
window.location.href = result.authorize_url;
});
};
@@ -824,79 +820,36 @@ export function SettingsPage() {
<AppCard
title="账号绑定"
description="支持绑定 GitHub、微信和邮箱地址,用于统一个人身份入口。"
description="支持绑定已启用的认证源和邮箱地址,用于统一个人身份入口。"
>
<div className="grid gap-6 xl:grid-cols-3">
<div className="grid gap-6 xl:grid-cols-2">
<div className="space-y-4 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<div className="space-y-1">
<p className="text-base font-semibold text-[var(--foreground-primary)]">
GitHub 账号
第三方认证源
</p>
<p className="text-sm leading-6 text-[var(--foreground-secondary)]">
当前状态:
{profile.github_id
? `已绑定 ${profile.github_id}`
: '未绑定'}
登录状态下发起授权会直接绑定到当前账号。
</p>
</div>
<PrimaryButton
type="button"
onClick={() =>
window.open(
`https://github.com/login/oauth/authorize?client_id=${publicStatus.github_client_id}&scope=user:email`,
'_blank',
'noopener,noreferrer',
)
}
disabled={
!publicStatus.github_oauth || !publicStatus.github_client_id
}
>
{publicStatus.github_oauth
? '绑定 GitHub'
: '未启用 GitHub OAuth'}
</PrimaryButton>
</div>
<div className="space-y-4 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<div className="space-y-1">
<p className="text-base font-semibold text-[var(--foreground-primary)]">
微信账号
</p>
<p className="text-sm leading-6 text-[var(--foreground-secondary)]">
当前状态:
{profile.wechat_id
? `已绑定 ${profile.wechat_id}`
: '未绑定'}
</p>
<div className="flex flex-wrap gap-3">
{(publicStatus.auth_sources ?? []).length > 0 ? (
publicStatus.auth_sources.map((source) => (
<PrimaryButton
key={source.id}
type="button"
onClick={() => handleBindAuthSource(source.name)}
disabled={busyKey === `auth-source-bind-${source.name}`}
>
绑定 {source.display_name || source.name}
</PrimaryButton>
))
) : (
<span className="text-sm text-[var(--foreground-secondary)]">
当前未启用认证源。
</span>
)}
</div>
{publicStatus.wechat_login && publicStatus.wechat_qrcode ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={publicStatus.wechat_qrcode}
alt="微信绑定二维码"
className="h-40 w-40 rounded-2xl border border-[var(--border-default)] object-cover"
/>
) : null}
<ResourceField
label="验证码"
hint="扫码关注后输入“验证码”获取绑定码。"
>
<ResourceInput
value={wechatCode}
onChange={(event) => setWeChatCode(event.target.value)}
placeholder="请输入微信验证码"
/>
</ResourceField>
<PrimaryButton
type="button"
onClick={handleBindWeChat}
disabled={
!publicStatus.wechat_login || busyKey === 'wechat-bind'
}
>
{busyKey === 'wechat-bind' ? '绑定中...' : '绑定微信'}
</PrimaryButton>
</div>
<div className="space-y-4 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
@@ -1279,8 +1232,7 @@ export function SettingsPage() {
</div>
)}
</AppCard>
<AppCard
title="版本与构建信息">
<AppCard title="版本与构建信息">
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
@@ -1396,7 +1348,7 @@ export function SettingsPage() {
</ResourceField>
<div className="mt-4 grid gap-4 md:grid-cols-3">
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] uppercase text-[var(--foreground-muted)]">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
触发频率
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
@@ -1404,7 +1356,7 @@ export function SettingsPage() {
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] uppercase text-[var(--foreground-muted)]">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
默认执行时间
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
@@ -1412,7 +1364,7 @@ export function SettingsPage() {
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] uppercase text-[var(--foreground-muted)]">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
生效范围
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
@@ -1490,8 +1442,16 @@ export function SettingsPage() {
<AppCard
title="登录与注册开关"
description="切换后立即生效,无需重启服务。"
action={
<SecondaryButton
type="button"
onClick={() => setAuthSourceModalOpen(true)}
>
配置认证源
</SecondaryButton>
}
>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<ToggleField
label="允许密码登录"
description="关闭后将无法使用用户名密码登录。"
@@ -1519,33 +1479,6 @@ export function SettingsPage() {
}
disabled={busyKey === 'toggle-EmailVerificationEnabled'}
/>
<ToggleField
label="启用 GitHub OAuth"
description="允许用户通过 GitHub 登录与注册。"
checked={systemFields.GitHubOAuthEnabled}
onChange={(checked) =>
handleToggleOption('GitHubOAuthEnabled', checked)
}
disabled={busyKey === 'toggle-GitHubOAuthEnabled'}
/>
<ToggleField
label="启用微信登录"
description="允许用户通过微信入口登录与注册。"
checked={systemFields.WeChatAuthEnabled}
onChange={(checked) =>
handleToggleOption('WeChatAuthEnabled', checked)
}
disabled={busyKey === 'toggle-WeChatAuthEnabled'}
/>
<ToggleField
label="启用 Turnstile"
description="开启后注册、邮箱验证码等流程需要先通过人机验证。"
checked={systemFields.TurnstileCheckEnabled}
onChange={(checked) =>
handleToggleOption('TurnstileCheckEnabled', checked)
}
disabled={busyKey === 'toggle-TurnstileCheckEnabled'}
/>
<ToggleField
label="允许新用户注册"
description="关闭后将禁止所有新用户注册入口。"
@@ -1556,6 +1489,9 @@ export function SettingsPage() {
disabled={busyKey === 'toggle-RegisterEnabled'}
/>
</div>
<div className="mt-5 text-sm text-[var(--foreground-secondary)]">
当前已配置 {authSourcesQuery.data?.length ?? 0} 个认证源。
</div>
</AppCard>
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
@@ -1694,145 +1630,6 @@ export function SettingsPage() {
</ResourceField>
</div>
</AppCard>
<AppCard
title="OAuth / WeChat / Turnstile"
description="敏感密钥不会从后端回显,留空即保持原值。"
action={
<PrimaryButton
type="button"
onClick={() =>
void runBusyAction('system-integrations', async () => {
await saveOptionEntries(
[
[
'GitHubClientId',
systemFields.GitHubClientId.trim(),
],
[
'GitHubClientSecret',
systemFields.GitHubClientSecret.trim(),
],
[
'WeChatServerAddress',
normalizeServerUrl(
systemFields.WeChatServerAddress,
),
],
[
'WeChatServerToken',
systemFields.WeChatServerToken.trim(),
],
[
'WeChatAccountQRCodeImageURL',
systemFields.WeChatAccountQRCodeImageURL.trim(),
],
[
'TurnstileSiteKey',
systemFields.TurnstileSiteKey.trim(),
],
[
'TurnstileSecretKey',
systemFields.TurnstileSecretKey.trim(),
],
],
'第三方集成设置已保存。',
);
})
}
disabled={busyKey === 'system-integrations'}
>
{busyKey === 'system-integrations'
? '保存中...'
: '保存集成设置'}
</PrimaryButton>
}
>
<div className="space-y-5">
<div className="grid gap-5 md:grid-cols-2">
<ResourceField label="GitHub Client ID">
<ResourceInput
value={systemFields.GitHubClientId}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
GitHubClientId: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="GitHub Client Secret">
<ResourceInput
type="password"
value={systemFields.GitHubClientSecret}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
GitHubClientSecret: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="WeChat Server 地址">
<ResourceInput
value={systemFields.WeChatServerAddress}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
WeChatServerAddress: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="WeChat Server Token">
<ResourceInput
type="password"
value={systemFields.WeChatServerToken}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
WeChatServerToken: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="公众号二维码链接">
<ResourceInput
value={systemFields.WeChatAccountQRCodeImageURL}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
WeChatAccountQRCodeImageURL: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="Turnstile Site Key">
<ResourceInput
value={systemFields.TurnstileSiteKey}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
TurnstileSiteKey: event.target.value,
}))
}
/>
</ResourceField>
<ResourceField label="Turnstile Secret Key">
<ResourceInput
type="password"
value={systemFields.TurnstileSecretKey}
onChange={(event) =>
setSystemFields((previous) => ({
...previous,
TurnstileSecretKey: event.target.value,
}))
}
/>
</ResourceField>
</div>
</div>
</AppCard>
</div>
<AppCard
title="请求限流设置"
@@ -2247,6 +2044,20 @@ export function SettingsPage() {
{renderTabContent()}
<AuthSourceModal
isOpen={authSourceModalOpen}
sources={authSourcesQuery.data ?? []}
isLoading={authSourcesQuery.isLoading}
error={authSourcesQuery.error}
onClose={() => setAuthSourceModalOpen(false)}
onChanged={async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: authSourcesQueryKey }),
queryClient.invalidateQueries({ queryKey: ['public-status'] }),
]);
}}
/>
<AppModal
isOpen={cleanupModalState !== null}
title={`清理${cleanupModalState?.label ?? ''}`}