From 7d9190a8d8163734b29542fb8c0363612be613e3 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 30 Mar 2026 16:00:14 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E6=96=B0=E7=94=A8=E6=88=B7=E6=B3=A8=E5=86=8C=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/app-config.md | 2 + openflare_server/common/constants.go | 2 +- openflare_server/controller/misc.go | 28 +- openflare_server/model/option_test.go | 30 + .../features/auth/components/login-form.tsx | 298 ++++++---- .../auth/components/register-form.tsx | 557 +++++++++++------- .../settings/components/settings-page.tsx | 4 +- openflare_server/web/types/public-status.ts | 12 +- 8 files changed, 559 insertions(+), 374 deletions(-) create mode 100644 openflare_server/model/option_test.go diff --git a/docs/app-config.md b/docs/app-config.md index 3f334e51..c84b0bdf 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -57,6 +57,8 @@ go run . --port 3000 --log-dir ./logs | `NodeOfflineThreshold` | 节点离线阈值(毫秒) | `120000` | | `AgentUpdateRepo` | Agent 自更新仓库 | `Rain-kl/OpenFlare` | | `GeoIPProvider` | 节点/IP 归属解析方式 | `ipinfo` | +| `RegisterEnabled` | 是否允许新用户注册 | `false` | +| `PasswordRegisterEnabled` | 是否允许通过密码方式注册 | `true` | | `DatabaseAutoCleanupEnabled` | 是否启用每日自动清理观测数据 | `false` | | `DatabaseAutoCleanupRetentionDays` | 自动清理保留天数(至少 1 天) | `30` | | `GlobalApiRateLimitNum` / `GlobalApiRateLimitDuration` | 全局 API 限流次数 / 时间窗口 | `300` / `180` | diff --git a/openflare_server/common/constants.go b/openflare_server/common/constants.go index f8877f5e..a8a1ca4d 100644 --- a/openflare_server/common/constants.go +++ b/openflare_server/common/constants.go @@ -31,7 +31,7 @@ var EmailVerificationEnabled = false var GitHubOAuthEnabled = false var WeChatAuthEnabled = false var TurnstileCheckEnabled = false -var RegisterEnabled = true +var RegisterEnabled = false var SMTPServer = "" var SMTPPort = 587 diff --git a/openflare_server/controller/misc.go b/openflare_server/controller/misc.go index f18f4679..68219c3d 100644 --- a/openflare_server/controller/misc.go +++ b/openflare_server/controller/misc.go @@ -23,19 +23,21 @@ func GetStatus(c *gin.Context) { "success": true, "message": "", "data": gin.H{ - "version": common.Version, - "start_time": common.StartTime, - "email_verification": common.EmailVerificationEnabled, - "github_oauth": common.GitHubOAuthEnabled, - "github_client_id": common.GitHubClientId, - "system_name": common.SystemName, - "home_page_link": common.HomePageLink, - "footer_html": common.Footer, - "wechat_qrcode": common.WeChatAccountQRCodeImageURL, - "wechat_login": common.WeChatAuthEnabled, - "server_address": common.ServerAddress, - "turnstile_check": common.TurnstileCheckEnabled, - "turnstile_site_key": common.TurnstileSiteKey, + "version": common.Version, + "start_time": common.StartTime, + "email_verification": common.EmailVerificationEnabled, + "github_oauth": common.GitHubOAuthEnabled, + "github_client_id": common.GitHubClientId, + "system_name": common.SystemName, + "home_page_link": common.HomePageLink, + "footer_html": common.Footer, + "wechat_qrcode": common.WeChatAccountQRCodeImageURL, + "wechat_login": common.WeChatAuthEnabled, + "server_address": common.ServerAddress, + "turnstile_check": common.TurnstileCheckEnabled, + "turnstile_site_key": common.TurnstileSiteKey, + "register_enabled": common.RegisterEnabled, + "password_register_enabled": common.PasswordRegisterEnabled, }, }) return diff --git a/openflare_server/model/option_test.go b/openflare_server/model/option_test.go new file mode 100644 index 00000000..a206adb0 --- /dev/null +++ b/openflare_server/model/option_test.go @@ -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") + } +} diff --git a/openflare_server/web/features/auth/components/login-form.tsx b/openflare_server/web/features/auth/components/login-form.tsx index 239a8f80..fbc83442 100644 --- a/openflare_server/web/features/auth/components/login-form.tsx +++ b/openflare_server/web/features/auth/components/login-form.tsx @@ -1,128 +1,170 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import Link from 'next/link'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { InlineMessage } from '@/components/feedback/inline-message'; -import { useAuth } from '@/components/providers/auth-provider'; -import { AppCard } from '@/components/ui/app-card'; -import { login } from '@/features/auth/api/auth'; -import { getPublicStatus } from '@/features/auth/api/public'; -import { - AuthButton, - AuthFormField, - AuthInput, - SecondaryButton, -} from '@/features/auth/components/auth-form-primitives'; -import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard'; - -const loginSchema = z.object({ - username: z.string().min(1, '请输入用户名'), - password: z.string().min(1, '请输入密码'), -}); - -type LoginFormValues = z.infer; - -export function LoginForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const { setUser } = useAuth(); - const [errorMessage, setErrorMessage] = useState(''); - const redirect = searchParams?.get('redirect') || '/'; - - const form = useForm({ - resolver: zodResolver(loginSchema), - defaultValues: { - username: '', - password: '', - }, - }); - - const statusQuery = useQuery({ - queryKey: ['public-status'], - queryFn: getPublicStatus, - }); - - const loginMutation = useMutation({ - mutationFn: login, - onSuccess: (user) => { - setUser(user); - router.replace(redirect); - }, - onError: (error: Error) => { - setErrorMessage(error.message || '登录失败,请稍后重试。'); - }, - }); - - const handleSubmit = form.handleSubmit((values) => { - setErrorMessage(''); - loginMutation.mutate(values); - }); - - const handleGitHubLogin = () => { - const clientId = statusQuery.data?.github_client_id; - if (!clientId) { - setErrorMessage('GitHub 登录当前不可用。'); - return; - } - - const authorizeUrl = new URL('https://github.com/login/oauth/authorize'); - authorizeUrl.searchParams.set('client_id', clientId); - authorizeUrl.searchParams.set('scope', 'user:email'); - window.location.href = authorizeUrl.toString(); - }; - - return ( - - -
- - - {form.formState.errors.username ? ( - - {form.formState.errors.username.message} - - ) : null} - - - - - {form.formState.errors.password ? ( - - {form.formState.errors.password.message} - - ) : null} - - - {errorMessage ? : null} - -
- - {loginMutation.isPending ? '登录中...' : '登录'} - - {statusQuery.data?.github_oauth ? ( - - GitHub 登录 - - ) : null} -
- - -
- - 忘记密码? - - · - - 注册 - -
-
-
- ); -} +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import Link from 'next/link'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { InlineMessage } from '@/components/feedback/inline-message'; +import { useAuth } from '@/components/providers/auth-provider'; +import { AppCard } from '@/components/ui/app-card'; +import { login } from '@/features/auth/api/auth'; +import { getPublicStatus } from '@/features/auth/api/public'; +import { + AuthButton, + AuthFormField, + AuthInput, + SecondaryButton, +} from '@/features/auth/components/auth-form-primitives'; +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({ + username: z.string().min(1, TEXT.usernameRequired), + password: z.string().min(1, TEXT.passwordRequired), +}); + +type LoginFormValues = z.infer; + +export function LoginForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { setUser } = useAuth(); + const [errorMessage, setErrorMessage] = useState(''); + const redirect = searchParams?.get('redirect') || '/'; + + const form = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + username: '', + password: '', + }, + }); + + const statusQuery = useQuery({ + queryKey: ['public-status'], + queryFn: getPublicStatus, + }); + + const canUsePasswordRegister = + (statusQuery.data?.register_enabled ?? false) && + (statusQuery.data?.password_register_enabled ?? false); + + const loginMutation = useMutation({ + mutationFn: login, + onSuccess: (user) => { + setUser(user); + router.replace(redirect); + }, + onError: (error: Error) => { + setErrorMessage(error.message || TEXT.loginFailed); + }, + }); + + const handleSubmit = form.handleSubmit((values) => { + setErrorMessage(''); + loginMutation.mutate(values); + }); + + const handleGitHubLogin = () => { + const clientId = statusQuery.data?.github_client_id; + if (!clientId) { + setErrorMessage(TEXT.githubUnavailable); + return; + } + + const authorizeUrl = new URL('https://github.com/login/oauth/authorize'); + authorizeUrl.searchParams.set('client_id', clientId); + authorizeUrl.searchParams.set('scope', 'user:email'); + window.location.href = authorizeUrl.toString(); + }; + + return ( + + +
+ + + {form.formState.errors.username ? ( + + {form.formState.errors.username.message} + + ) : null} + + + + + {form.formState.errors.password ? ( + + {form.formState.errors.password.message} + + ) : null} + + + {errorMessage ? ( + + ) : null} + +
+ + {loginMutation.isPending ? TEXT.loginPending : TEXT.login} + + {statusQuery.data?.github_oauth ? ( + + {TEXT.githubLogin} + + ) : null} +
+ + +
+ + {TEXT.forgotPassword} + + {canUsePasswordRegister ? ( + <> + | + + {TEXT.register} + + + ) : null} +
+
+
+ ); +} diff --git a/openflare_server/web/features/auth/components/register-form.tsx b/openflare_server/web/features/auth/components/register-form.tsx index 3d54ab13..e167ac89 100644 --- a/openflare_server/web/features/auth/components/register-form.tsx +++ b/openflare_server/web/features/auth/components/register-form.tsx @@ -1,225 +1,332 @@ -'use client'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { useMutation, useQuery } from '@tanstack/react-query'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useMemo, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { InlineMessage } from '@/components/feedback/inline-message'; -import { TurnstileWidget } from '@/components/forms/turnstile-widget'; -import { AppCard } from '@/components/ui/app-card'; -import { - register as registerUser, - sendEmailVerification, -} from '@/features/auth/api/auth'; -import { getPublicStatus } from '@/features/auth/api/public'; -import { - AuthButton, - AuthFormField, - AuthInput, - SecondaryButton, -} from '@/features/auth/components/auth-form-primitives'; -import { PublicAuthGuard } from '@/features/auth/components/public-auth-guard'; - -const baseSchemaObject = z.object({ - username: z.string().min(1, '请输入用户名').max(12, '用户名最长 12 位'), - password: z.string().min(8, '密码至少 8 位').max(20, '密码最长 20 位'), - password2: z.string().min(8, '请再次输入密码'), - email: z.string().optional(), - verification_code: z.string().optional(), -}); - -const baseSchema = baseSchemaObject.refine((data) => data.password === data.password2, { - message: '两次输入的密码不一致', - path: ['password2'], -}); - -type RegisterFormValues = z.infer; - -export function RegisterForm() { - const router = useRouter(); - const [turnstileToken, setTurnstileToken] = useState(''); - const [message, setMessage] = useState<{ tone: 'success' | 'danger' | 'info'; text: string } | null>(null); - - const statusQuery = useQuery({ - queryKey: ['public-status'], - queryFn: getPublicStatus, - }); - - const needsEmailVerification = statusQuery.data?.email_verification ?? false; - const needsTurnstile = statusQuery.data?.turnstile_check ?? false; - - const schema = useMemo(() => { - if (!needsEmailVerification) { - return baseSchema; - } - - return baseSchemaObject - .extend({ - email: z.string().email('请输入有效邮箱地址'), - verification_code: z.string().min(1, '请输入验证码'), - }) - .refine((data) => data.password === data.password2, { - message: '两次输入的密码不一致', - path: ['password2'], - }); - }, [needsEmailVerification]); - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - username: '', - password: '', - password2: '', - email: '', - verification_code: '', - }, - }); - - const registerMutation = useMutation({ - mutationFn: (values: RegisterFormValues) => - registerUser( - { - username: values.username, - password: values.password, - email: values.email, - verification_code: values.verification_code, - }, - turnstileToken || undefined, - ), - onSuccess: () => { - router.replace('/login'); - }, - onError: (error: Error) => { - setMessage({ tone: 'danger', text: error.message || '注册失败,请稍后重试。' }); - }, - }); - - const verificationMutation = useMutation({ - mutationFn: async () => { - const email = form.getValues('email'); - if (!email) { - form.setError('email', { message: '请输入邮箱地址' }); - return; - } - await sendEmailVerification(email, turnstileToken || undefined); - }, - onSuccess: () => { - setMessage({ tone: 'success', text: '验证码发送成功,请检查邮箱。' }); - }, - onError: (error: Error) => { - setMessage({ tone: 'danger', text: error.message || '验证码发送失败,请稍后重试。' }); - }, - }); - - const handleSubmit = form.handleSubmit((values) => { - setMessage(null); - if (needsTurnstile && !turnstileToken) { - setMessage({ tone: 'info', text: '请先完成人机验证。' }); - return; - } - registerMutation.mutate(values); - }); - - return ( - - -
- - - {form.formState.errors.username ? ( - - {form.formState.errors.username.message} - - ) : null} - - - - - {form.formState.errors.password ? ( - - {form.formState.errors.password.message} - - ) : null} - - - - - {form.formState.errors.password2 ? ( - - {form.formState.errors.password2.message} - - ) : null} - - - {needsEmailVerification ? ( - <> - - - {form.formState.errors.email ? ( - - {form.formState.errors.email.message} - - ) : null} - - - -
- - { - if (needsTurnstile && !turnstileToken) { - setMessage({ tone: 'info', text: '请先完成人机验证。' }); - return; - } - setMessage(null); - verificationMutation.mutate(); - }} - disabled={verificationMutation.isPending} - > - {verificationMutation.isPending ? '发送中...' : '获取验证码'} - -
- {form.formState.errors.verification_code ? ( - - {form.formState.errors.verification_code.message} - - ) : null} -
- - ) : null} - - {needsTurnstile && statusQuery.data?.turnstile_site_key ? ( - setTurnstileToken(token)} - onExpire={() => setTurnstileToken('')} - onError={() => setTurnstileToken('')} - /> - ) : null} - - {message ? : null} - - - {registerMutation.isPending ? '注册中...' : '注册'} - - - -
- 已有账户? - - 点击登录 - -
-
-
- ); -} +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useMemo, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { InlineMessage } from '@/components/feedback/inline-message'; +import { TurnstileWidget } from '@/components/forms/turnstile-widget'; +import { AppCard } from '@/components/ui/app-card'; +import { + register as registerUser, + sendEmailVerification, +} from '@/features/auth/api/auth'; +import { getPublicStatus } from '@/features/auth/api/public'; +import { + AuthButton, + AuthFormField, + AuthInput, + SecondaryButton, +} from '@/features/auth/components/auth-form-primitives'; +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({ + username: z.string().min(1, TEXT.usernameRequired).max(12, TEXT.usernameTooLong), + password: z.string().min(8, TEXT.passwordTooShort).max(20, TEXT.passwordTooLong), + password2: z.string().min(8, TEXT.passwordRepeatRequired), + email: z.string().optional(), + verification_code: z.string().optional(), +}); + +const baseSchema = baseSchemaObject.refine( + (data) => data.password === data.password2, + { + message: TEXT.passwordMismatch, + path: ['password2'], + }, +); + +type RegisterFormValues = z.infer; + +export function RegisterForm() { + const router = useRouter(); + const [turnstileToken, setTurnstileToken] = useState(''); + const [message, setMessage] = useState<{ + tone: 'success' | 'danger' | 'info'; + text: string; + } | null>(null); + + const statusQuery = useQuery({ + queryKey: ['public-status'], + queryFn: getPublicStatus, + }); + + const needsEmailVerification = statusQuery.data?.email_verification ?? 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(() => { + if (!needsEmailVerification) { + return baseSchema; + } + + return baseSchemaObject + .extend({ + email: z.string().email(TEXT.emailInvalid), + verification_code: z.string().min(1, TEXT.codeRequired), + }) + .refine((data) => data.password === data.password2, { + message: TEXT.passwordMismatch, + path: ['password2'], + }); + }, [needsEmailVerification]); + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + username: '', + password: '', + password2: '', + email: '', + verification_code: '', + }, + }); + + const registerMutation = useMutation({ + mutationFn: (values: RegisterFormValues) => + registerUser( + { + username: values.username, + password: values.password, + email: values.email, + verification_code: values.verification_code, + }, + turnstileToken || undefined, + ), + onSuccess: () => { + router.replace('/login'); + }, + onError: (error: Error) => { + setMessage({ tone: 'danger', text: error.message || TEXT.registerFailed }); + }, + }); + + const verificationMutation = useMutation({ + mutationFn: async () => { + const email = form.getValues('email'); + if (!email) { + form.setError('email', { message: TEXT.emailRequired }); + return; + } + await sendEmailVerification(email, turnstileToken || undefined); + }, + onSuccess: () => { + setMessage({ tone: 'success', text: TEXT.verificationSent }); + }, + onError: (error: Error) => { + setMessage({ + tone: 'danger', + text: error.message || TEXT.verificationFailed, + }); + }, + }); + + const handleSubmit = form.handleSubmit((values) => { + setMessage(null); + if (needsTurnstile && !turnstileToken) { + setMessage({ tone: 'info', text: TEXT.turnstileRequired }); + return; + } + registerMutation.mutate(values); + }); + + return ( + + + {!registerEnabled ? ( +
+ +
+ {TEXT.hasAccount} + + {TEXT.clickLogin} + +
+
+ ) : !passwordRegisterEnabled ? ( +
+ +
+ {TEXT.backToLogin} + + {TEXT.clickLogin} + +
+
+ ) : ( + <> +
+ + + {form.formState.errors.username ? ( + + {form.formState.errors.username.message} + + ) : null} + + + + + {form.formState.errors.password ? ( + + {form.formState.errors.password.message} + + ) : null} + + + + + {form.formState.errors.password2 ? ( + + {form.formState.errors.password2.message} + + ) : null} + + + {needsEmailVerification ? ( + <> + + + {form.formState.errors.email ? ( + + {form.formState.errors.email.message} + + ) : null} + + + +
+ + { + if (needsTurnstile && !turnstileToken) { + setMessage({ + tone: 'info', + text: TEXT.turnstileRequired, + }); + return; + } + setMessage(null); + verificationMutation.mutate(); + }} + disabled={verificationMutation.isPending} + > + {verificationMutation.isPending + ? TEXT.gettingCode + : TEXT.getCode} + +
+ {form.formState.errors.verification_code ? ( + + {form.formState.errors.verification_code.message} + + ) : null} +
+ + ) : null} + + {needsTurnstile && statusQuery.data?.turnstile_site_key ? ( + setTurnstileToken(token)} + onExpire={() => setTurnstileToken('')} + onError={() => setTurnstileToken('')} + /> + ) : null} + + {message ? ( + + ) : null} + + + {registerMutation.isPending ? TEXT.registering : TEXT.register} + + + +
+ {TEXT.hasAccount} + + {TEXT.clickLogin} + +
+ + )} +
+
+ ); +} diff --git a/openflare_server/web/features/settings/components/settings-page.tsx b/openflare_server/web/features/settings/components/settings-page.tsx index 091f4763..395c6d97 100644 --- a/openflare_server/web/features/settings/components/settings-page.tsx +++ b/openflare_server/web/features/settings/components/settings-page.tsx @@ -61,7 +61,7 @@ const defaultSystemFields = { GitHubOAuthEnabled: false, WeChatAuthEnabled: false, TurnstileCheckEnabled: false, - RegisterEnabled: true, + RegisterEnabled: false, SMTPServer: '', SMTPPort: '587', SMTPAccount: '', @@ -348,7 +348,7 @@ export function SettingsPage() { GitHubOAuthEnabled: toBoolean(optionMap.GitHubOAuthEnabled, false), WeChatAuthEnabled: toBoolean(optionMap.WeChatAuthEnabled, false), TurnstileCheckEnabled: toBoolean(optionMap.TurnstileCheckEnabled, false), - RegisterEnabled: toBoolean(optionMap.RegisterEnabled, true), + RegisterEnabled: toBoolean(optionMap.RegisterEnabled, false), SMTPServer: optionMap.SMTPServer ?? '', SMTPPort: optionMap.SMTPPort ?? '587', SMTPAccount: optionMap.SMTPAccount ?? '', diff --git a/openflare_server/web/types/public-status.ts b/openflare_server/web/types/public-status.ts index 96a9276b..260d31ec 100644 --- a/openflare_server/web/types/public-status.ts +++ b/openflare_server/web/types/public-status.ts @@ -8,8 +8,10 @@ export interface PublicStatus { home_page_link: string; footer_html: string; wechat_qrcode: string; - wechat_login: boolean; - server_address: string; - turnstile_check: boolean; - turnstile_site_key: string; -} + wechat_login: boolean; + server_address: string; + turnstile_check: boolean; + turnstile_site_key: string; + register_enabled: boolean; + password_register_enabled: boolean; +}