mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 00:56:37 +08:00
[优化] 移除注册相关功能的代码和配置
This commit is contained in:
@@ -44,8 +44,6 @@ The settings page maintains these hot-updatable options:
|
|||||||
| `NodeOfflineThreshold` | Node offline threshold in milliseconds | `120000` |
|
| `NodeOfflineThreshold` | Node offline threshold in milliseconds | `120000` |
|
||||||
| `AgentUpdateRepo` | Agent update repository | `Rain-kl/OpenFlare` |
|
| `AgentUpdateRepo` | Agent update repository | `Rain-kl/OpenFlare` |
|
||||||
| `GeoIPProvider` | Node/IP region provider | `ipinfo` |
|
| `GeoIPProvider` | Node/IP region provider | `ipinfo` |
|
||||||
| `RegisterEnabled` | Allow new user registration | `false` |
|
|
||||||
| `PasswordRegisterEnabled` | Allow password registration | `true` |
|
|
||||||
| `DatabaseAutoCleanupEnabled` | Enable daily observability cleanup | `false` |
|
| `DatabaseAutoCleanupEnabled` | Enable daily observability cleanup | `false` |
|
||||||
| `DatabaseAutoCleanupRetentionDays` | Retention days | `30` |
|
| `DatabaseAutoCleanupRetentionDays` | Retention days | `30` |
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,6 @@ 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` |
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"openflare/common"
|
"openflare/common"
|
||||||
"openflare/model"
|
"openflare/model"
|
||||||
@@ -12,6 +9,10 @@ import (
|
|||||||
"openflare/utils/validation"
|
"openflare/utils/validation"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
@@ -115,70 +116,9 @@ func Logout(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(c *gin.Context) {
|
func Register(c *gin.Context) {
|
||||||
if !common.RegisterEnabled {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"message": "管理员关闭了新用户注册",
|
|
||||||
"success": false,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !common.PasswordRegisterEnabled {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"message": "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册",
|
|
||||||
"success": false,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var user model.User
|
|
||||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "无效的参数",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := validation.Validate.Struct(&user); err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "输入不合法 " + err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if common.EmailVerificationEnabled {
|
|
||||||
if user.Email == "" || user.VerificationCode == "" {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "管理员开启了邮箱验证,请输入邮箱地址和验证码",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !security.VerifyCodeWithKey(user.Email, user.VerificationCode, security.EmailVerificationPurpose) {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": "验证码错误或已过期",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cleanUser := model.User{
|
|
||||||
Username: user.Username,
|
|
||||||
Password: user.Password,
|
|
||||||
DisplayName: user.Username,
|
|
||||||
}
|
|
||||||
if common.EmailVerificationEnabled {
|
|
||||||
cleanUser.Email = user.Email
|
|
||||||
}
|
|
||||||
if err := cleanUser.Insert(); err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"message": "非法请求",
|
||||||
"message": "",
|
"success": false,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
||||||
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
||||||
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
|
||||||
common.OptionMap["SMTPServer"] = ""
|
common.OptionMap["SMTPServer"] = ""
|
||||||
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort)
|
||||||
common.OptionMap["SMTPAccount"] = ""
|
common.OptionMap["SMTPAccount"] = ""
|
||||||
@@ -187,8 +186,6 @@ func updateOptionMap(key string, value string) {
|
|||||||
common.WeChatAuthEnabled = boolValue
|
common.WeChatAuthEnabled = boolValue
|
||||||
case "TurnstileCheckEnabled":
|
case "TurnstileCheckEnabled":
|
||||||
common.TurnstileCheckEnabled = boolValue
|
common.TurnstileCheckEnabled = boolValue
|
||||||
case "RegisterEnabled":
|
|
||||||
common.RegisterEnabled = boolValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch key {
|
switch key {
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -382,8 +382,8 @@ func TestPhase1HTTPSAndCertificateImportLifecycle(t *testing.T) {
|
|||||||
t.Fatalf("expected active config to expose main_config, got %#v", activeConfig["main_config"])
|
t.Fatalf("expected active config to expose main_config, got %#v", activeConfig["main_config"])
|
||||||
}
|
}
|
||||||
supportFiles, ok := activeConfig["support_files"].([]any)
|
supportFiles, ok := activeConfig["support_files"].([]any)
|
||||||
if !ok || len(supportFiles) != 2 {
|
if !ok || len(supportFiles) != 3 {
|
||||||
t.Fatalf("expected active config to expose 2 support files, got %#v", activeConfig["support_files"])
|
t.Fatalf("expected active config to expose 3 support files, got %#v", activeConfig["support_files"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,11 @@ package service
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"openflare/common"
|
|
||||||
"openflare/model"
|
"openflare/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCompleteOAuthLoginRequiresLinkWhenRegistrationDisabled(t *testing.T) {
|
func TestCompleteOAuthLoginRequiresLinkWhenRegistrationDisabled(t *testing.T) {
|
||||||
setupServiceTestDB(t)
|
setupServiceTestDB(t)
|
||||||
previousRegisterEnabled := common.RegisterEnabled
|
|
||||||
common.RegisterEnabled = false
|
|
||||||
t.Cleanup(func() {
|
|
||||||
common.RegisterEnabled = previousRegisterEnabled
|
|
||||||
})
|
|
||||||
|
|
||||||
source := createTestAuthSource(t)
|
source := createTestAuthSource(t)
|
||||||
result, pending, err := CompleteOAuthLogin(source, &OAuthProfile{
|
result, pending, err := CompleteOAuthLogin(source, &OAuthProfile{
|
||||||
@@ -48,43 +42,10 @@ func TestCompleteOAuthLoginRequiresLinkWhenRegistrationDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteOAuthLoginAutoRegistersWhenEnabled(t *testing.T) {
|
|
||||||
setupServiceTestDB(t)
|
|
||||||
previousRegisterEnabled := common.RegisterEnabled
|
|
||||||
common.RegisterEnabled = true
|
|
||||||
t.Cleanup(func() {
|
|
||||||
common.RegisterEnabled = previousRegisterEnabled
|
|
||||||
})
|
|
||||||
|
|
||||||
source := createTestAuthSource(t)
|
|
||||||
result, pending, err := CompleteOAuthLogin(source, &OAuthProfile{
|
|
||||||
ExternalID: "external-2",
|
|
||||||
ExternalUsername: "oidc-user",
|
|
||||||
DisplayName: "OIDC User",
|
|
||||||
Email: "oidc@example.com",
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CompleteOAuthLogin failed: %v", err)
|
|
||||||
}
|
|
||||||
if pending != nil {
|
|
||||||
t.Fatalf("expected no pending account when registration is enabled")
|
|
||||||
}
|
|
||||||
if result.Status != "registered" || result.User == nil {
|
|
||||||
t.Fatalf("expected registered user result, got %#v", result)
|
|
||||||
}
|
|
||||||
account, err := model.FindExternalAccount(source.ID, "external-2")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected external account to be linked: %v", err)
|
|
||||||
}
|
|
||||||
if account.UserID != result.User.Id {
|
|
||||||
t.Fatalf("expected external account user %d, got %d", result.User.Id, account.UserID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTestAuthSource(t *testing.T) *model.AuthSource {
|
func createTestAuthSource(t *testing.T) *model.AuthSource {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
source := &model.AuthSource{
|
source := &model.AuthSource{
|
||||||
Name: "Test OIDC",
|
Name: "test-oidc",
|
||||||
Type: model.AuthSourceTypeOIDC,
|
Type: model.AuthSourceTypeOIDC,
|
||||||
DisplayName: "Test OIDC",
|
DisplayName: "Test OIDC",
|
||||||
ClientID: "client-id",
|
ClientID: "client-id",
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Suspense } from 'react';
|
|
||||||
|
|
||||||
import { LoadingState } from '@/components/feedback/loading-state';
|
|
||||||
import { RegisterForm } from '@/features/auth/components/register-form';
|
|
||||||
|
|
||||||
export default function RegisterPage() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<LoadingState />}>
|
|
||||||
<RegisterForm />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -63,10 +63,6 @@ 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) => {
|
||||||
@@ -166,17 +162,6 @@ export function LoginForm() {
|
|||||||
>
|
>
|
||||||
{TEXT.forgotPassword}
|
{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>
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
'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<typeof baseSchema>;
|
|
||||||
|
|
||||||
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<RegisterFormValues>({
|
|
||||||
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 (
|
|
||||||
<PublicAuthGuard>
|
|
||||||
<AppCard title={TEXT.title} description={TEXT.description}>
|
|
||||||
{!registerEnabled ? (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<InlineMessage tone='info' message={TEXT.registerClosed} />
|
|
||||||
<div className='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>
|
|
||||||
</div>
|
|
||||||
) : !passwordRegisterEnabled ? (
|
|
||||||
<div className='space-y-4'>
|
|
||||||
<InlineMessage tone='info' message={TEXT.passwordRegisterClosed} />
|
|
||||||
<div className='text-sm text-[var(--foreground-secondary)]'>
|
|
||||||
{TEXT.backToLogin}
|
|
||||||
<Link
|
|
||||||
href='/login'
|
|
||||||
className='ml-2 text-[var(--brand-primary)] transition hover:opacity-80'
|
|
||||||
>
|
|
||||||
{TEXT.clickLogin}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<form className='space-y-4' onSubmit={handleSubmit}>
|
|
||||||
<AuthFormField label={TEXT.username} hint={TEXT.usernameHint}>
|
|
||||||
<AuthInput
|
|
||||||
placeholder={TEXT.username}
|
|
||||||
{...form.register('username')}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.username ? (
|
|
||||||
<span className='text-xs text-[var(--status-danger-foreground)]'>
|
|
||||||
{form.formState.errors.username.message}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</AuthFormField>
|
|
||||||
|
|
||||||
<AuthFormField label={TEXT.password} hint={TEXT.passwordHint}>
|
|
||||||
<AuthInput
|
|
||||||
type='password'
|
|
||||||
placeholder={TEXT.password}
|
|
||||||
{...form.register('password')}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.password ? (
|
|
||||||
<span className='text-xs text-[var(--status-danger-foreground)]'>
|
|
||||||
{form.formState.errors.password.message}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</AuthFormField>
|
|
||||||
|
|
||||||
<AuthFormField label={TEXT.passwordConfirm}>
|
|
||||||
<AuthInput
|
|
||||||
type='password'
|
|
||||||
placeholder={TEXT.passwordConfirm}
|
|
||||||
{...form.register('password2')}
|
|
||||||
/>
|
|
||||||
{form.formState.errors.password2 ? (
|
|
||||||
<span className='text-xs text-[var(--status-danger-foreground)]'>
|
|
||||||
{form.formState.errors.password2.message}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</AuthFormField>
|
|
||||||
|
|
||||||
{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>
|
|
||||||
|
|
||||||
<AuthFormField label={TEXT.emailCode}>
|
|
||||||
<div className='flex flex-col gap-3 sm:flex-row'>
|
|
||||||
<AuthInput
|
|
||||||
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}
|
|
||||||
|
|
||||||
{needsTurnstile && statusQuery.data?.turnstile_site_key ? (
|
|
||||||
<TurnstileWidget
|
|
||||||
siteKey={statusQuery.data.turnstile_site_key}
|
|
||||||
onVerify={(token) => setTurnstileToken(token)}
|
|
||||||
onExpire={() => setTurnstileToken('')}
|
|
||||||
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>
|
|
||||||
</PublicAuthGuard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -72,7 +72,6 @@ const defaultSystemFields = {
|
|||||||
GitHubOAuthEnabled: false,
|
GitHubOAuthEnabled: false,
|
||||||
WeChatAuthEnabled: false,
|
WeChatAuthEnabled: false,
|
||||||
TurnstileCheckEnabled: false,
|
TurnstileCheckEnabled: false,
|
||||||
RegisterEnabled: false,
|
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '587',
|
SMTPPort: '587',
|
||||||
SMTPAccount: '',
|
SMTPAccount: '',
|
||||||
@@ -365,7 +364,6 @@ 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, false),
|
|
||||||
SMTPServer: optionMap.SMTPServer ?? '',
|
SMTPServer: optionMap.SMTPServer ?? '',
|
||||||
SMTPPort: optionMap.SMTPPort ?? '587',
|
SMTPPort: optionMap.SMTPPort ?? '587',
|
||||||
SMTPAccount: optionMap.SMTPAccount ?? '',
|
SMTPAccount: optionMap.SMTPAccount ?? '',
|
||||||
@@ -1571,15 +1569,7 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
disabled={busyKey === 'toggle-EmailVerificationEnabled'}
|
disabled={busyKey === 'toggle-EmailVerificationEnabled'}
|
||||||
/>
|
/>
|
||||||
<ToggleField
|
|
||||||
label="允许新用户注册"
|
|
||||||
description="关闭后将禁止所有新用户注册入口。"
|
|
||||||
checked={systemFields.RegisterEnabled}
|
|
||||||
onChange={(checked) =>
|
|
||||||
handleToggleOption('RegisterEnabled', checked)
|
|
||||||
}
|
|
||||||
disabled={busyKey === 'toggle-RegisterEnabled'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 text-sm text-[var(--foreground-secondary)]">
|
<div className="mt-5 text-sm text-[var(--foreground-secondary)]">
|
||||||
当前已配置 {authSourcesQuery.data?.length ?? 0} 个认证源。
|
当前已配置 {authSourcesQuery.data?.length ?? 0} 个认证源。
|
||||||
|
|||||||
Reference in New Issue
Block a user