[功能] 禁用新用户注册功能,更新相关逻辑和测试

This commit is contained in:
ryan
2026-03-30 16:00:14 +08:00
parent 4b1e75f86b
commit 7d9190a8d8
8 changed files with 559 additions and 374 deletions
+2
View File
@@ -57,6 +57,8 @@ go run . --port 3000 --log-dir ./logs
| `NodeOfflineThreshold` | 节点离线阈值(毫秒) | `120000` | | `NodeOfflineThreshold` | 节点离线阈值(毫秒) | `120000` |
| `AgentUpdateRepo` | Agent 自更新仓库 | `Rain-kl/OpenFlare` | | `AgentUpdateRepo` | Agent 自更新仓库 | `Rain-kl/OpenFlare` |
| `GeoIPProvider` | 节点/IP 归属解析方式 | `ipinfo` | | `GeoIPProvider` | 节点/IP 归属解析方式 | `ipinfo` |
| `RegisterEnabled` | 是否允许新用户注册 | `false` |
| `PasswordRegisterEnabled` | 是否允许通过密码方式注册 | `true` |
| `DatabaseAutoCleanupEnabled` | 是否启用每日自动清理观测数据 | `false` | | `DatabaseAutoCleanupEnabled` | 是否启用每日自动清理观测数据 | `false` |
| `DatabaseAutoCleanupRetentionDays` | 自动清理保留天数(至少 1 天) | `30` | | `DatabaseAutoCleanupRetentionDays` | 自动清理保留天数(至少 1 天) | `30` |
| `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | 全局 API 限流次数 / 时间窗口 | `300` / `180` | | `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | 全局 API 限流次数 / 时间窗口 | `300` / `180` |
+1 -1
View File
@@ -31,7 +31,7 @@ var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false var GitHubOAuthEnabled = false
var WeChatAuthEnabled = false var WeChatAuthEnabled = false
var TurnstileCheckEnabled = false var TurnstileCheckEnabled = false
var RegisterEnabled = true var RegisterEnabled = false
var SMTPServer = "" var SMTPServer = ""
var SMTPPort = 587 var SMTPPort = 587
+15 -13
View File
@@ -23,19 +23,21 @@ func GetStatus(c *gin.Context) {
"success": true, "success": true,
"message": "", "message": "",
"data": gin.H{ "data": gin.H{
"version": common.Version, "version": common.Version,
"start_time": common.StartTime, "start_time": common.StartTime,
"email_verification": common.EmailVerificationEnabled, "email_verification": common.EmailVerificationEnabled,
"github_oauth": common.GitHubOAuthEnabled, "github_oauth": common.GitHubOAuthEnabled,
"github_client_id": common.GitHubClientId, "github_client_id": common.GitHubClientId,
"system_name": common.SystemName, "system_name": common.SystemName,
"home_page_link": common.HomePageLink, "home_page_link": common.HomePageLink,
"footer_html": common.Footer, "footer_html": common.Footer,
"wechat_qrcode": common.WeChatAccountQRCodeImageURL, "wechat_qrcode": common.WeChatAccountQRCodeImageURL,
"wechat_login": common.WeChatAuthEnabled, "wechat_login": common.WeChatAuthEnabled,
"server_address": common.ServerAddress, "server_address": common.ServerAddress,
"turnstile_check": common.TurnstileCheckEnabled, "turnstile_check": common.TurnstileCheckEnabled,
"turnstile_site_key": common.TurnstileSiteKey, "turnstile_site_key": common.TurnstileSiteKey,
"register_enabled": common.RegisterEnabled,
"password_register_enabled": common.PasswordRegisterEnabled,
}, },
}) })
return return
+30
View File
@@ -0,0 +1,30 @@
package model
import (
"openflare/common"
"testing"
)
func TestInitOptionMapDefaultsRegisterDisabled(t *testing.T) {
previousRegisterEnabled := common.RegisterEnabled
previousOptionMap := common.OptionMap
previousDB := DB
t.Cleanup(func() {
common.RegisterEnabled = previousRegisterEnabled
common.OptionMap = previousOptionMap
DB = previousDB
})
DB = openTestSQLiteDB(t, "options-defaults.db")
common.RegisterEnabled = false
common.OptionMap = nil
InitOptionMap()
if got := common.OptionMap["RegisterEnabled"]; got != "false" {
t.Fatalf("expected RegisterEnabled default to be false, got %q", got)
}
if common.RegisterEnabled {
t.Fatal("expected RegisterEnabled to remain false after InitOptionMap")
}
}
@@ -21,9 +21,24 @@ import {
} from '@/features/auth/components/auth-form-primitives'; } from '@/features/auth/components/auth-form-primitives';
import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard'; import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard';
const TEXT = {
usernameRequired: '\u8bf7\u8f93\u5165\u7528\u6237\u540d',
passwordRequired: '\u8bf7\u8f93\u5165\u5bc6\u7801',
loginFailed: '\u767b\u5f55\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002',
githubUnavailable: 'GitHub \u767b\u5f55\u5f53\u524d\u4e0d\u53ef\u7528\u3002',
title: '\u7528\u6237\u767b\u5f55',
username: '\u7528\u6237\u540d',
password: '\u5bc6\u7801',
loginPending: '\u767b\u5f55\u4e2d...',
login: '\u767b\u5f55',
githubLogin: 'GitHub \u767b\u5f55',
forgotPassword: '\u5fd8\u8bb0\u5bc6\u7801\uff1f',
register: '\u6ce8\u518c',
};
const loginSchema = z.object({ const loginSchema = z.object({
username: z.string().min(1, '请输入用户名'), username: z.string().min(1, TEXT.usernameRequired),
password: z.string().min(1, '请输入密码'), password: z.string().min(1, TEXT.passwordRequired),
}); });
type LoginFormValues = z.infer<typeof loginSchema>; type LoginFormValues = z.infer<typeof loginSchema>;
@@ -48,6 +63,10 @@ export function LoginForm() {
queryFn: getPublicStatus, queryFn: getPublicStatus,
}); });
const canUsePasswordRegister =
(statusQuery.data?.register_enabled ?? false) &&
(statusQuery.data?.password_register_enabled ?? false);
const loginMutation = useMutation({ const loginMutation = useMutation({
mutationFn: login, mutationFn: login,
onSuccess: (user) => { onSuccess: (user) => {
@@ -55,7 +74,7 @@ export function LoginForm() {
router.replace(redirect); router.replace(redirect);
}, },
onError: (error: Error) => { onError: (error: Error) => {
setErrorMessage(error.message || '登录失败,请稍后重试。'); setErrorMessage(error.message || TEXT.loginFailed);
}, },
}); });
@@ -67,7 +86,7 @@ export function LoginForm() {
const handleGitHubLogin = () => { const handleGitHubLogin = () => {
const clientId = statusQuery.data?.github_client_id; const clientId = statusQuery.data?.github_client_id;
if (!clientId) { if (!clientId) {
setErrorMessage('GitHub 登录当前不可用。'); setErrorMessage(TEXT.githubUnavailable);
return; return;
} }
@@ -79,10 +98,13 @@ export function LoginForm() {
return ( return (
<PublicAuthGuard> <PublicAuthGuard>
<AppCard title='用户登录'> <AppCard title={TEXT.title}>
<form className='space-y-4' onSubmit={handleSubmit}> <form className='space-y-4' onSubmit={handleSubmit}>
<AuthFormField label='用户名'> <AuthFormField label={TEXT.username}>
<AuthInput placeholder='请输入用户名' {...form.register('username')} /> <AuthInput
placeholder={TEXT.username}
{...form.register('username')}
/>
{form.formState.errors.username ? ( {form.formState.errors.username ? (
<span className='text-xs text-[var(--status-danger-foreground)]'> <span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.username.message} {form.formState.errors.username.message}
@@ -90,8 +112,12 @@ export function LoginForm() {
) : null} ) : null}
</AuthFormField> </AuthFormField>
<AuthFormField label='密码'> <AuthFormField label={TEXT.password}>
<AuthInput type='password' placeholder='请输入密码' {...form.register('password')} /> <AuthInput
type='password'
placeholder={TEXT.password}
{...form.register('password')}
/>
{form.formState.errors.password ? ( {form.formState.errors.password ? (
<span className='text-xs text-[var(--status-danger-foreground)]'> <span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.password.message} {form.formState.errors.password.message}
@@ -99,28 +125,44 @@ export function LoginForm() {
) : null} ) : null}
</AuthFormField> </AuthFormField>
{errorMessage ? <InlineMessage tone='danger' message={errorMessage} /> : null} {errorMessage ? (
<InlineMessage tone='danger' message={errorMessage} />
) : null}
<div className='flex flex-col gap-3 sm:flex-row'> <div className='flex flex-col gap-3 sm:flex-row'>
<AuthButton type='submit' disabled={loginMutation.isPending}> <AuthButton type='submit' disabled={loginMutation.isPending}>
{loginMutation.isPending ? '登录中...' : '登录'} {loginMutation.isPending ? TEXT.loginPending : TEXT.login}
</AuthButton> </AuthButton>
{statusQuery.data?.github_oauth ? ( {statusQuery.data?.github_oauth ? (
<SecondaryButton type='button' onClick={handleGitHubLogin} className='w-full sm:w-auto'> <SecondaryButton
GitHub 登录 type='button'
onClick={handleGitHubLogin}
className='w-full sm:w-auto'
>
{TEXT.githubLogin}
</SecondaryButton> </SecondaryButton>
) : null} ) : null}
</div> </div>
</form> </form>
<div className='mt-6 flex flex-wrap gap-3 text-sm text-[var(--foreground-secondary)]'> <div className='mt-6 flex flex-wrap gap-3 text-sm text-[var(--foreground-secondary)]'>
<Link href='/reset' className='text-[var(--brand-primary)] transition hover:opacity-80'> <Link
忘记密码? href='/reset'
</Link> className='text-[var(--brand-primary)] transition hover:opacity-80'
<span>·</span> >
<Link href='/register' className='text-[var(--brand-primary)] transition hover:opacity-80'> {TEXT.forgotPassword}
注册
</Link> </Link>
{canUsePasswordRegister ? (
<>
<span>|</span>
<Link
href='/register'
className='text-[var(--brand-primary)] transition hover:opacity-80'
>
{TEXT.register}
</Link>
</>
) : null}
</div> </div>
</AppCard> </AppCard>
</PublicAuthGuard> </PublicAuthGuard>
@@ -24,25 +24,71 @@ import {
} from '@/features/auth/components/auth-form-primitives'; } from '@/features/auth/components/auth-form-primitives';
import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard'; import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard';
const TEXT = {
title: '\u65b0\u7528\u6237\u6ce8\u518c',
description:
'\u517c\u5bb9\u73b0\u6709\u5bc6\u7801\u6ce8\u518c\u94fe\u8def\uff0c\u540e\u7eed\u53ef\u7ee7\u7eed\u6269\u5c55\u7b2c\u4e09\u65b9\u6ce8\u518c\u3002',
usernameRequired: '\u8bf7\u8f93\u5165\u7528\u6237\u540d',
usernameTooLong: '\u7528\u6237\u540d\u6700\u957f 12 \u4f4d',
passwordTooShort: '\u5bc6\u7801\u81f3\u5c11 8 \u4f4d',
passwordTooLong: '\u5bc6\u7801\u6700\u957f 20 \u4f4d',
passwordRepeatRequired: '\u8bf7\u518d\u6b21\u8f93\u5165\u5bc6\u7801',
passwordMismatch: '\u4e24\u6b21\u8f93\u5165\u7684\u5bc6\u7801\u4e0d\u4e00\u81f4',
emailInvalid: '\u8bf7\u8f93\u5165\u6709\u6548\u90ae\u7bb1\u5730\u5740',
codeRequired: '\u8bf7\u8f93\u5165\u9a8c\u8bc1\u7801',
registerFailed: '\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002',
emailRequired: '\u8bf7\u8f93\u5165\u90ae\u7bb1\u5730\u5740',
verificationSent:
'\u9a8c\u8bc1\u7801\u53d1\u9001\u6210\u529f\uff0c\u8bf7\u68c0\u67e5\u90ae\u7bb1\u3002',
verificationFailed:
'\u9a8c\u8bc1\u7801\u53d1\u9001\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002',
turnstileRequired: '\u8bf7\u5148\u5b8c\u6210\u4eba\u673a\u9a8c\u8bc1\u3002',
registerClosed:
'\u7ba1\u7406\u5458\u5df2\u5173\u95ed\u65b0\u7528\u6237\u6ce8\u518c\u3002',
passwordRegisterClosed:
'\u7ba1\u7406\u5458\u5df2\u5173\u95ed\u5bc6\u7801\u6ce8\u518c\uff0c\u8bf7\u4f7f\u7528\u7b2c\u4e09\u65b9\u767b\u5f55\u5165\u53e3\u5b8c\u6210\u6ce8\u518c\u3002',
hasAccount: '\u5df2\u6709\u8d26\u53f7\uff1f',
backToLogin:
'\u8fd4\u56de\u767b\u5f55\u9875\u67e5\u770b\u53ef\u7528\u5165\u53e3\uff1a',
clickLogin: '\u70b9\u51fb\u767b\u5f55',
username: '\u7528\u6237\u540d',
usernameHint: '\u6700\u957f 12 \u4f4d',
password: '\u5bc6\u7801',
passwordHint: '\u6700\u77ed 8 \u4f4d\uff0c\u6700\u957f 20 \u4f4d',
passwordConfirm: '\u786e\u8ba4\u5bc6\u7801',
email: '\u90ae\u7bb1\u5730\u5740',
emailCode: '\u90ae\u7bb1\u9a8c\u8bc1\u7801',
getCode: '\u83b7\u53d6\u9a8c\u8bc1\u7801',
gettingCode: '\u53d1\u9001\u4e2d...',
register: '\u6ce8\u518c',
registering: '\u6ce8\u518c\u4e2d...',
};
const baseSchemaObject = z.object({ const baseSchemaObject = z.object({
username: z.string().min(1, '请输入用户名').max(12, '用户名最长 12 位'), username: z.string().min(1, TEXT.usernameRequired).max(12, TEXT.usernameTooLong),
password: z.string().min(8, '密码至少 8 位').max(20, '密码最长 20 位'), password: z.string().min(8, TEXT.passwordTooShort).max(20, TEXT.passwordTooLong),
password2: z.string().min(8, '请再次输入密码'), password2: z.string().min(8, TEXT.passwordRepeatRequired),
email: z.string().optional(), email: z.string().optional(),
verification_code: z.string().optional(), verification_code: z.string().optional(),
}); });
const baseSchema = baseSchemaObject.refine((data) => data.password === data.password2, { const baseSchema = baseSchemaObject.refine(
message: '两次输入的密码不一致', (data) => data.password === data.password2,
path: ['password2'], {
}); message: TEXT.passwordMismatch,
path: ['password2'],
},
);
type RegisterFormValues = z.infer<typeof baseSchema>; type RegisterFormValues = z.infer<typeof baseSchema>;
export function RegisterForm() { export function RegisterForm() {
const router = useRouter(); const router = useRouter();
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
const [message, setMessage] = useState<{ tone: 'success' | 'danger' | 'info'; text: string } | null>(null); const [message, setMessage] = useState<{
tone: 'success' | 'danger' | 'info';
text: string;
} | null>(null);
const statusQuery = useQuery({ const statusQuery = useQuery({
queryKey: ['public-status'], queryKey: ['public-status'],
@@ -51,6 +97,9 @@ export function RegisterForm() {
const needsEmailVerification = statusQuery.data?.email_verification ?? false; const needsEmailVerification = statusQuery.data?.email_verification ?? false;
const needsTurnstile = statusQuery.data?.turnstile_check ?? false; const needsTurnstile = statusQuery.data?.turnstile_check ?? false;
const registerEnabled = statusQuery.data?.register_enabled ?? false;
const passwordRegisterEnabled =
statusQuery.data?.password_register_enabled ?? false;
const schema = useMemo(() => { const schema = useMemo(() => {
if (!needsEmailVerification) { if (!needsEmailVerification) {
@@ -59,11 +108,11 @@ export function RegisterForm() {
return baseSchemaObject return baseSchemaObject
.extend({ .extend({
email: z.string().email('请输入有效邮箱地址'), email: z.string().email(TEXT.emailInvalid),
verification_code: z.string().min(1, '请输入验证码'), verification_code: z.string().min(1, TEXT.codeRequired),
}) })
.refine((data) => data.password === data.password2, { .refine((data) => data.password === data.password2, {
message: '两次输入的密码不一致', message: TEXT.passwordMismatch,
path: ['password2'], path: ['password2'],
}); });
}, [needsEmailVerification]); }, [needsEmailVerification]);
@@ -94,7 +143,7 @@ export function RegisterForm() {
router.replace('/login'); router.replace('/login');
}, },
onError: (error: Error) => { onError: (error: Error) => {
setMessage({ tone: 'danger', text: error.message || '注册失败,请稍后重试。' }); setMessage({ tone: 'danger', text: error.message || TEXT.registerFailed });
}, },
}); });
@@ -102,23 +151,26 @@ export function RegisterForm() {
mutationFn: async () => { mutationFn: async () => {
const email = form.getValues('email'); const email = form.getValues('email');
if (!email) { if (!email) {
form.setError('email', { message: '请输入邮箱地址' }); form.setError('email', { message: TEXT.emailRequired });
return; return;
} }
await sendEmailVerification(email, turnstileToken || undefined); await sendEmailVerification(email, turnstileToken || undefined);
}, },
onSuccess: () => { onSuccess: () => {
setMessage({ tone: 'success', text: '验证码发送成功,请检查邮箱。' }); setMessage({ tone: 'success', text: TEXT.verificationSent });
}, },
onError: (error: Error) => { onError: (error: Error) => {
setMessage({ tone: 'danger', text: error.message || '验证码发送失败,请稍后重试。' }); setMessage({
tone: 'danger',
text: error.message || TEXT.verificationFailed,
});
}, },
}); });
const handleSubmit = form.handleSubmit((values) => { const handleSubmit = form.handleSubmit((values) => {
setMessage(null); setMessage(null);
if (needsTurnstile && !turnstileToken) { if (needsTurnstile && !turnstileToken) {
setMessage({ tone: 'info', text: '请先完成人机验证。' }); setMessage({ tone: 'info', text: TEXT.turnstileRequired });
return; return;
} }
registerMutation.mutate(values); registerMutation.mutate(values);
@@ -126,99 +178,154 @@ export function RegisterForm() {
return ( return (
<PublicAuthGuard> <PublicAuthGuard>
<AppCard title='新用户注册' description='兼容现有密码注册链路,后续可继续扩展第三方注册。'> <AppCard title={TEXT.title} description={TEXT.description}>
<form className='space-y-4' onSubmit={handleSubmit}> {!registerEnabled ? (
<AuthFormField label='用户名' hint='最长 12 位'> <div className='space-y-4'>
<AuthInput placeholder='请输入用户名' {...form.register('username')} /> <InlineMessage tone='info' message={TEXT.registerClosed} />
{form.formState.errors.username ? ( <div className='text-sm text-[var(--foreground-secondary)]'>
<span className='text-xs text-[var(--status-danger-foreground)]'> {TEXT.hasAccount}
{form.formState.errors.username.message} <Link
</span> href='/login'
) : null} className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'
</AuthFormField> >
{TEXT.clickLogin}
<AuthFormField label='密码' hint='最短 8 位,最长 20 位'> </Link>
<AuthInput type='password' placeholder='请输入密码' {...form.register('password')} /> </div>
{form.formState.errors.password ? ( </div>
<span className='text-xs text-[var(--status-danger-foreground)]'> ) : !passwordRegisterEnabled ? (
{form.formState.errors.password.message} <div className='space-y-4'>
</span> <InlineMessage tone='info' message={TEXT.passwordRegisterClosed} />
) : null} <div className='text-sm text-[var(--foreground-secondary)]'>
</AuthFormField> {TEXT.backToLogin}
<Link
<AuthFormField label='确认密码'> href='/login'
<AuthInput type='password' placeholder='请再次输入密码' {...form.register('password2')} /> className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'
{form.formState.errors.password2 ? ( >
<span className='text-xs text-[var(--status-danger-foreground)]'> {TEXT.clickLogin}
{form.formState.errors.password2.message} </Link>
</span> </div>
) : null} </div>
</AuthFormField> ) : (
<>
{needsEmailVerification ? ( <form className='space-y-4' onSubmit={handleSubmit}>
<> <AuthFormField label={TEXT.username} hint={TEXT.usernameHint}>
<AuthFormField label='邮箱地址'> <AuthInput
<AuthInput type='email' placeholder='请输入邮箱地址' {...form.register('email')} /> placeholder={TEXT.username}
{form.formState.errors.email ? ( {...form.register('username')}
/>
{form.formState.errors.username ? (
<span className='text-xs text-[var(--status-danger-foreground)]'> <span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.email.message} {form.formState.errors.username.message}
</span> </span>
) : null} ) : null}
</AuthFormField> </AuthFormField>
<AuthFormField label='邮箱验证码'> <AuthFormField label={TEXT.password} hint={TEXT.passwordHint}>
<div className='flex flex-col gap-3 sm:flex-row'> <AuthInput
<AuthInput type='password'
placeholder='请输入验证码' placeholder={TEXT.password}
className='flex-1' {...form.register('password')}
{...form.register('verification_code')} />
/> {form.formState.errors.password ? (
<SecondaryButton
type='button'
onClick={() => {
if (needsTurnstile && !turnstileToken) {
setMessage({ tone: 'info', text: '请先完成人机验证。' });
return;
}
setMessage(null);
verificationMutation.mutate();
}}
disabled={verificationMutation.isPending}
>
{verificationMutation.isPending ? '发送中...' : '获取验证码'}
</SecondaryButton>
</div>
{form.formState.errors.verification_code ? (
<span className='text-xs text-[var(--status-danger-foreground)]'> <span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.verification_code.message} {form.formState.errors.password.message}
</span> </span>
) : null} ) : null}
</AuthFormField> </AuthFormField>
</>
) : null}
{needsTurnstile && statusQuery.data?.turnstile_site_key ? ( <AuthFormField label={TEXT.passwordConfirm}>
<TurnstileWidget <AuthInput
siteKey={statusQuery.data.turnstile_site_key} type='password'
onVerify={(token) => setTurnstileToken(token)} placeholder={TEXT.passwordConfirm}
onExpire={() => setTurnstileToken('')} {...form.register('password2')}
onError={() => setTurnstileToken('')} />
/> {form.formState.errors.password2 ? (
) : null} <span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.password2.message}
</span>
) : null}
</AuthFormField>
{message ? <InlineMessage tone={message.tone} message={message.text} /> : null} {needsEmailVerification ? (
<>
<AuthFormField label={TEXT.email}>
<AuthInput
type='email'
placeholder={TEXT.email}
{...form.register('email')}
/>
{form.formState.errors.email ? (
<span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.email.message}
</span>
) : null}
</AuthFormField>
<AuthButton type='submit' disabled={registerMutation.isPending}> <AuthFormField label={TEXT.emailCode}>
{registerMutation.isPending ? '注册中...' : '注册'} <div className='flex flex-col gap-3 sm:flex-row'>
</AuthButton> <AuthInput
</form> placeholder={TEXT.emailCode}
className='flex-1'
{...form.register('verification_code')}
/>
<SecondaryButton
type='button'
onClick={() => {
if (needsTurnstile && !turnstileToken) {
setMessage({
tone: 'info',
text: TEXT.turnstileRequired,
});
return;
}
setMessage(null);
verificationMutation.mutate();
}}
disabled={verificationMutation.isPending}
>
{verificationMutation.isPending
? TEXT.gettingCode
: TEXT.getCode}
</SecondaryButton>
</div>
{form.formState.errors.verification_code ? (
<span className='text-xs text-[var(--status-danger-foreground)]'>
{form.formState.errors.verification_code.message}
</span>
) : null}
</AuthFormField>
</>
) : null}
<div className='mt-6 text-sm text-[var(--foreground-secondary)]'> {needsTurnstile && statusQuery.data?.turnstile_site_key ? (
已有账户? <TurnstileWidget
<Link href='/login' className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'> siteKey={statusQuery.data.turnstile_site_key}
点击登录 onVerify={(token) => setTurnstileToken(token)}
</Link> onExpire={() => setTurnstileToken('')}
</div> onError={() => setTurnstileToken('')}
/>
) : null}
{message ? (
<InlineMessage tone={message.tone} message={message.text} />
) : null}
<AuthButton type='submit' disabled={registerMutation.isPending}>
{registerMutation.isPending ? TEXT.registering : TEXT.register}
</AuthButton>
</form>
<div className='mt-6 text-sm text-[var(--foreground-secondary)]'>
{TEXT.hasAccount}
<Link
href='/login'
className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'
>
{TEXT.clickLogin}
</Link>
</div>
</>
)}
</AppCard> </AppCard>
</PublicAuthGuard> </PublicAuthGuard>
); );
@@ -61,7 +61,7 @@ const defaultSystemFields = {
GitHubOAuthEnabled: false, GitHubOAuthEnabled: false,
WeChatAuthEnabled: false, WeChatAuthEnabled: false,
TurnstileCheckEnabled: false, TurnstileCheckEnabled: false,
RegisterEnabled: true, RegisterEnabled: false,
SMTPServer: '', SMTPServer: '',
SMTPPort: '587', SMTPPort: '587',
SMTPAccount: '', SMTPAccount: '',
@@ -348,7 +348,7 @@ export function SettingsPage() {
GitHubOAuthEnabled: toBoolean(optionMap.GitHubOAuthEnabled, false), GitHubOAuthEnabled: toBoolean(optionMap.GitHubOAuthEnabled, false),
WeChatAuthEnabled: toBoolean(optionMap.WeChatAuthEnabled, false), WeChatAuthEnabled: toBoolean(optionMap.WeChatAuthEnabled, false),
TurnstileCheckEnabled: toBoolean(optionMap.TurnstileCheckEnabled, false), TurnstileCheckEnabled: toBoolean(optionMap.TurnstileCheckEnabled, false),
RegisterEnabled: toBoolean(optionMap.RegisterEnabled, true), RegisterEnabled: toBoolean(optionMap.RegisterEnabled, false),
SMTPServer: optionMap.SMTPServer ?? '', SMTPServer: optionMap.SMTPServer ?? '',
SMTPPort: optionMap.SMTPPort ?? '587', SMTPPort: optionMap.SMTPPort ?? '587',
SMTPAccount: optionMap.SMTPAccount ?? '', SMTPAccount: optionMap.SMTPAccount ?? '',
@@ -12,4 +12,6 @@ export interface PublicStatus {
server_address: string; server_address: string;
turnstile_check: boolean; turnstile_check: boolean;
turnstile_site_key: string; turnstile_site_key: string;
register_enabled: boolean;
password_register_enabled: boolean;
} }