mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 02:56:22 +00:00
refactor: restructure authentication components and routes
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 13s
All checks were successful
Push to Helm Chart Repository / build (push) Successful in 13s
- Removed the old reset password form component and replaced it with a new implementation. - Updated routing paths for login, registration, and reset password to be under a common auth path. - Added new login and registration pages with corresponding forms. - Introduced a common auth header component for consistent branding across auth pages. - Implemented a current logged-in user display component. - Enhanced the register form to include email verification and captcha. - Updated translations for new and modified components. - Refactored the navigation bar to include user avatar dropdown and improved menu structure.
This commit is contained in:
13
TODO.md
13
TODO.md
@ -1,12 +1,7 @@
|
|||||||
# TODO List
|
# TODO List
|
||||||
|
|
||||||
## 主页模块
|
## 代办
|
||||||
|
|
||||||
- [ ]主页文章列表
|
- [ ] 给邮件验证码加冷却时间,前后端都加
|
||||||
- [ ]主页侧边栏卡片列表
|
- [ ] 优化评论区的时间显示
|
||||||
|
- [ ] 在当前有用户登录时,使用OIDC会自动绑定
|
||||||
## 评论模块
|
|
||||||
|
|
||||||
- [ ]评论列表
|
|
||||||
- [ ]评论输入框
|
|
||||||
- [ ]评论回复功能
|
|
@ -52,6 +52,12 @@ func (u *UserController) Register(ctx context.Context, c *app.RequestContext) {
|
|||||||
resps.BadRequest(c, resps.ErrParamInvalid)
|
resps.BadRequest(c, resps.ErrParamInvalid)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail)))
|
||||||
|
if email == "" {
|
||||||
|
resps.BadRequest(c, "Email header is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userRegisterReq.Email = email
|
||||||
resp, err := u.service.UserRegister(&userRegisterReq)
|
resp, err := u.service.UserRegister(&userRegisterReq)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -97,7 +103,7 @@ func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) {
|
|||||||
Code: code,
|
Code: code,
|
||||||
State: state,
|
State: state,
|
||||||
}
|
}
|
||||||
resp, err := u.service.OidcLogin(oidcLoginReq)
|
resp, err := u.service.OidcLogin(ctx, oidcLoginReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serviceErr := errs.AsServiceError(err)
|
serviceErr := errs.AsServiceError(err)
|
||||||
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)
|
||||||
|
@ -32,8 +32,7 @@ type UserRegisterReq struct {
|
|||||||
Username string `json:"username"` // 用户名
|
Username string `json:"username"` // 用户名
|
||||||
Nickname string `json:"nickname"` // 昵称
|
Nickname string `json:"nickname"` // 昵称
|
||||||
Password string `json:"password"` // 密码
|
Password string `json:"password"` // 密码
|
||||||
Email string `json:"email"` // 邮箱
|
Email string `json:"-" binding:"-"`
|
||||||
VerificationCode string `json:"verification_code"` // 邮箱验证码
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegisterResp struct {
|
type UserRegisterResp struct {
|
||||||
|
@ -2,6 +2,7 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
"github.com/snowykami/neo-blog/pkg/constant"
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
@ -12,8 +13,8 @@ import (
|
|||||||
// UseEmailVerify 中间件函数,用于邮箱验证,使用前先调用请求发送邮件验证码函数
|
// UseEmailVerify 中间件函数,用于邮箱验证,使用前先调用请求发送邮件验证码函数
|
||||||
func UseEmailVerify() app.HandlerFunc {
|
func UseEmailVerify() app.HandlerFunc {
|
||||||
return func(ctx context.Context, c *app.RequestContext) {
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
email := string(c.GetHeader(constant.HeaderKeyEmail))
|
email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail)))
|
||||||
verifyCode := string(c.GetHeader(constant.HeaderKeyVerifyCode))
|
verifyCode := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyVerifyCode)))
|
||||||
if !utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
|
if !utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
|
||||||
c.Next(ctx)
|
c.Next(ctx)
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ func registerUserRoutes(group *route.RouterGroup) {
|
|||||||
{
|
{
|
||||||
userGroupWithoutAuthNeedsCaptcha.POST("/login", userController.Login)
|
userGroupWithoutAuthNeedsCaptcha.POST("/login", userController.Login)
|
||||||
userGroupWithoutAuthNeedsCaptcha.POST("/register", userController.Register)
|
userGroupWithoutAuthNeedsCaptcha.POST("/register", userController.Register)
|
||||||
userGroupWithoutAuthNeedsCaptcha.POST("/email/verify", userController.VerifyEmail) // Send email verification code
|
userGroupWithoutAuth.POST("/email/verify", userController.VerifyEmail) // Send email verification code
|
||||||
userGroupWithoutAuth.GET("/captcha", userController.GetCaptchaConfig)
|
userGroupWithoutAuth.GET("/captcha", userController.GetCaptchaConfig)
|
||||||
userGroupWithoutAuth.GET("/oidc/list", userController.OidcList)
|
userGroupWithoutAuth.GET("/oidc/list", userController.OidcList)
|
||||||
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)
|
||||||
|
@ -56,16 +56,9 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) {
|
func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) {
|
||||||
// 验证邮箱验证码
|
|
||||||
if !utils.Env.GetAsBool(constant.EnvKeyEnableRegister, true) {
|
if !utils.Env.GetAsBool(constant.EnvKeyEnableRegister, true) {
|
||||||
return nil, errs.ErrForbidden
|
return nil, errs.ErrForbidden
|
||||||
}
|
}
|
||||||
if utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
|
|
||||||
ok := utils.VerifyEmailCode(req.Email, req.VerificationCode)
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.New(http.StatusForbidden, "Invalid email verification code", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 检查用户名或邮箱是否已存在
|
// 检查用户名或邮箱是否已存在
|
||||||
usernameExist, err := repo.User.CheckUsernameExists(req.Username)
|
usernameExist, err := repo.User.CheckUsernameExists(req.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -185,7 +178,7 @@ func (s *UserService) ListOidcConfigs() ([]dto.UserOidcConfigDto, error) {
|
|||||||
return oidcConfigsDtos, nil
|
return oidcConfigsDtos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) OidcLogin(req *dto.OidcLoginReq) (*dto.OidcLoginResp, error) {
|
func (s *UserService) OidcLogin(ctx context.Context, req *dto.OidcLoginReq) (*dto.OidcLoginResp, error) {
|
||||||
// 验证state
|
// 验证state
|
||||||
kvStore := utils.KV.GetInstance()
|
kvStore := utils.KV.GetInstance()
|
||||||
storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State)
|
storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { OidcConfig } from '@/models/oidc-config'
|
import type { OidcConfig } from '@/models/oidc-config'
|
||||||
import type { BaseResponse } from '@/models/resp'
|
import type { BaseResponse } from '@/models/resp'
|
||||||
import type { RegisterRequest, User } from '@/models/user'
|
import type { User } from '@/models/user'
|
||||||
import { CaptchaProvider } from '@/models/captcha'
|
import { CaptchaProvider } from '@/models/captcha'
|
||||||
import axiosClient from './client'
|
import axiosClient from './client'
|
||||||
|
|
||||||
@ -31,11 +31,18 @@ export async function userLogout(): Promise<BaseResponse<null>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function userRegister(
|
export async function userRegister(
|
||||||
data: RegisterRequest,
|
{ username, password, email, verifyCode, captchaToken }: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
email: string
|
||||||
|
verifyCode?: string
|
||||||
|
captchaToken?: string
|
||||||
|
},
|
||||||
): Promise<BaseResponse<{ token: string, user: User }>> {
|
): Promise<BaseResponse<{ token: string, user: User }>> {
|
||||||
const res = await axiosClient.post<BaseResponse<{ token: string, user: User }>>(
|
const res = await axiosClient.post<BaseResponse<{ token: string, user: User }>>(
|
||||||
'/user/register',
|
'/user/register',
|
||||||
data,
|
{ username, password, },
|
||||||
|
{ headers: { 'X-Email': email, 'X-VerifyCode': verifyCode || '' , 'X-Captcha-Token': captchaToken || ''} },
|
||||||
)
|
)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { motion } from 'motion/react'
|
import { motion } from 'motion/react'
|
||||||
import { Navbar } from '@/components/layout/navbar-or-side'
|
import { Navbar } from '@/components/layout/nav/navbar-or-side'
|
||||||
import { BackgroundProvider } from '@/contexts/background-context'
|
import { BackgroundProvider } from '@/contexts/background-context'
|
||||||
import Footer from '@/components/layout/footer'
|
import Footer from '@/components/layout/footer'
|
||||||
import config from '@/config'
|
import config from '@/config'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { LoginForm } from '@/components/login/login-form'
|
import { LoginForm } from '@/components/auth/login/login-form'
|
||||||
import { AuthHeader } from '@/components/common/auth-header'
|
import { AuthHeader } from '@/components/auth/common/auth-header'
|
||||||
|
|
||||||
function LoginPageContent() {
|
function LoginPageContent() {
|
||||||
return (
|
return (
|
40
web/src/app/auth/register/page.tsx
Normal file
40
web/src/app/auth/register/page.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Suspense } from 'react'
|
||||||
|
import { AuthHeader } from '@/components/auth/common/auth-header'
|
||||||
|
import { RegisterForm } from '@/components/auth/register/register-form'
|
||||||
|
|
||||||
|
function PageContent() {
|
||||||
|
return (
|
||||||
|
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||||
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||||
|
<AuthHeader />
|
||||||
|
<RegisterForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={(
|
||||||
|
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
||||||
|
<div className="flex w-full max-w-sm flex-col gap-6">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="flex items-center gap-3 self-center mb-6">
|
||||||
|
<div className="size-10 bg-gray-300 rounded-full"></div>
|
||||||
|
<div className="h-8 bg-gray-300 rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white rounded-lg p-6 space-y-4">
|
||||||
|
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-gray-300 rounded w-1/2"></div>
|
||||||
|
<div className="h-10 bg-gray-300 rounded"></div>
|
||||||
|
<div className="h-10 bg-gray-300 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PageContent />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { AuthHeader } from "@/components/common/auth-header";
|
import { AuthHeader } from "@/components/auth/common/auth-header";
|
||||||
import { ResetPasswordForm } from "@/components/reset-password/reset-password-form";
|
import { ResetPasswordForm } from "@/components/auth/reset-password/reset-password-form";
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">
|
42
web/src/components/auth/common/current-logged.tsx
Normal file
42
web/src/components/auth/common/current-logged.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
|
import { getGravatarFromUser } from "@/utils/common/gravatar";
|
||||||
|
import { formatDisplayName, getFallbackAvatarFromUsername } from "@/utils/common/username";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import React from "react";
|
||||||
|
import { SectionDivider } from '@/components/common/section-divider';
|
||||||
|
|
||||||
|
export function CurrentLogged() {
|
||||||
|
const t = useTranslations("Login");
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const redirectBack = searchParams.get("redirect_back") || "/"
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const handleLoggedContinue = () => {
|
||||||
|
router.push(redirectBack);
|
||||||
|
}
|
||||||
|
if (!user) return null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SectionDivider className="mb-4">{t("currently_logged_in")}</SectionDivider>
|
||||||
|
<div onClick={handleLoggedContinue} className="cursor-pointer">
|
||||||
|
<div className="flex gap-2 justify-center items-center">
|
||||||
|
<Avatar className="h-10 w-10 rounded-full">
|
||||||
|
<AvatarImage src={getGravatarFromUser({ user })} alt={user.username} />
|
||||||
|
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className="grid place-items-center text-sm leading-tight text-center">
|
||||||
|
<span className="text-primary font-medium">{formatDisplayName(user)}</span>
|
||||||
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
|
{user.email}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -18,18 +18,19 @@ import { getCaptchaConfig, listOidcConfigs, userLogin } from "@/api/user"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Captcha from "../common/captcha"
|
import Captcha from "@/components/common/captcha"
|
||||||
import { CaptchaProvider } from "@/models/captcha"
|
import { CaptchaProvider } from "@/models/captcha"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useAuth } from "@/contexts/auth-context"
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
import { resetPasswordPath, useToResetPassword } from "@/hooks/use-route"
|
import { registerPath, resetPasswordPath } from "@/hooks/use-route"
|
||||||
|
import { CurrentLogged } from "@/components/auth/common/current-logged"
|
||||||
|
import { SectionDivider } from "@/components/common/section-divider"
|
||||||
|
|
||||||
export function LoginForm({
|
export function LoginForm({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
const t = useTranslations('Login')
|
const t = useTranslations('Login')
|
||||||
const toResetPassword = useToResetPassword();
|
|
||||||
const { user, setUser } = useAuth();
|
const { user, setUser } = useAuth();
|
||||||
const [oidcConfigs, setOidcConfigs] = useState<OidcConfig[]>([])
|
const [oidcConfigs, setOidcConfigs] = useState<OidcConfig[]>([])
|
||||||
const [captchaProps, setCaptchaProps] = useState<{
|
const [captchaProps, setCaptchaProps] = useState<{
|
||||||
@ -45,12 +46,6 @@ export function LoginForm({
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const redirectBack = searchParams.get("redirect_back") || "/"
|
const redirectBack = searchParams.get("redirect_back") || "/"
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
router.push(redirectBack);
|
|
||||||
}
|
|
||||||
}, [user, router, redirectBack]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listOidcConfigs()
|
listOidcConfigs()
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -105,11 +100,10 @@ export function LoginForm({
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-xl">{t("welcome")}</CardTitle>
|
<CardTitle className="text-xl">{t("welcome")}</CardTitle>
|
||||||
<CardDescription>
|
|
||||||
{t("with_oidc")}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<CurrentLogged />
|
||||||
|
<SectionDivider className="my-4">{t("with_oidc")}</SectionDivider>
|
||||||
<form>
|
<form>
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
{/* OIDC 登录选项 */}
|
{/* OIDC 登录选项 */}
|
||||||
@ -134,16 +128,10 @@ export function LoginForm({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 分隔线 */}
|
{/* 分隔线 */}
|
||||||
{oidcConfigs.length > 0 && (
|
{oidcConfigs.length > 0 && (
|
||||||
<div className="after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t">
|
<SectionDivider className="my-0"> {t("or_continue_with_local_account")}</SectionDivider>
|
||||||
<span className="bg-card text-muted-foreground relative z-10 px-2">
|
|
||||||
{t("or_continue_with_local_account")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 邮箱密码登录 */}
|
{/* 邮箱密码登录 */}
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
@ -193,7 +181,7 @@ export function LoginForm({
|
|||||||
{/* 注册链接 */}
|
{/* 注册链接 */}
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
{t("no_account")}{" "}
|
{t("no_account")}{" "}
|
||||||
<Link href="#" className="underline underline-offset-4">
|
<Link href={registerPath+"?redirect_back="+encodeURIComponent(redirectBack)} className="underline underline-offset-4">
|
||||||
{t("register")}
|
{t("register")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -201,18 +189,6 @@ export function LoginForm({
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 服务条款 */}
|
|
||||||
<div className="text-muted-foreground text-center text-xs text-balance">
|
|
||||||
{t("by_logging_in_you_agree_to_our")}{" "}
|
|
||||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
|
||||||
{t("terms_of_service")}
|
|
||||||
</a>{" "}
|
|
||||||
{t("and")}{" "}
|
|
||||||
<a href="#" className="underline underline-offset-4 hover:text-primary">
|
|
||||||
{t("privacy_policy")}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
187
web/src/components/auth/register/register-form.tsx
Normal file
187
web/src/components/auth/register/register-form.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { getCaptchaConfig, requestEmailVerifyCode, userRegister } from "@/api/user"
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import Captcha from "@/components/common/captcha"
|
||||||
|
import { CaptchaProvider } from "@/models/captcha"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { CurrentLogged } from "@/components/auth/common/current-logged"
|
||||||
|
import { SectionDivider } from "@/components/common/section-divider"
|
||||||
|
import { InputOTPControlled } from "@/components/common/input-otp"
|
||||||
|
import { BaseErrorResponse } from "@/models/resp"
|
||||||
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
|
|
||||||
|
export function RegisterForm({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
const { setUser } = useAuth();
|
||||||
|
const t = useTranslations('Register')
|
||||||
|
const commonT = useTranslations('Common')
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const redirectBack = searchParams.get("redirect_back") || "/"
|
||||||
|
const [captchaProps, setCaptchaProps] = useState<{
|
||||||
|
provider: CaptchaProvider
|
||||||
|
siteKey: string
|
||||||
|
url?: string
|
||||||
|
} | null>(null)
|
||||||
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||||
|
const [isLogging, setIsLogging] = useState(false)
|
||||||
|
const [refreshCaptchaKey, setRefreshCaptchaKey] = useState(0)
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [verifyCode, setVerifyCode] = useState('')
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCaptchaConfig()
|
||||||
|
.then((res) => {
|
||||||
|
setCaptchaProps(res.data)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(t("fetch_captcha_config_failed") + (error?.message ? `: ${error.message}` : ""))
|
||||||
|
setCaptchaProps(null)
|
||||||
|
})
|
||||||
|
}, [refreshCaptchaKey, t])
|
||||||
|
|
||||||
|
const handleCaptchaError = (error: string) => {
|
||||||
|
toast.error(t("captcha_error") + (error ? `: ${error}` : ""));
|
||||||
|
setTimeout(() => {
|
||||||
|
setRefreshCaptchaKey(k => k + 1);
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendVerifyCode = () => {
|
||||||
|
requestEmailVerifyCode(email)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(t("send_verify_code_success"))
|
||||||
|
})
|
||||||
|
.catch((error: BaseErrorResponse) => {
|
||||||
|
toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRegister = () => {
|
||||||
|
if (!username || !password || !email) {
|
||||||
|
toast.error(t("please_fill_in_all_required_fields"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!captchaToken) {
|
||||||
|
toast.error(t("please_complete_captcha_verification"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
userRegister({ username, password, email, verifyCode, captchaToken })
|
||||||
|
.then(res => {
|
||||||
|
toast.success(t("register_success") + ` ${res.data.user.nickname || res.data.user.username}`);
|
||||||
|
setUser(res.data.user);
|
||||||
|
router.push(redirectBack)
|
||||||
|
})
|
||||||
|
.catch((error: BaseErrorResponse) => {
|
||||||
|
toast.error(t("register_failed") + (error?.response?.data?.message ? `: ${error.response.data.message}` : ""))
|
||||||
|
setRefreshCaptchaKey(k => k + 1)
|
||||||
|
setCaptchaToken(null)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLogging(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<CardTitle className="text-xl">{t("title")}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CurrentLogged />
|
||||||
|
<form>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<SectionDivider className="mt-4">{t("register_a_new_account")}</SectionDivider>
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
|
||||||
|
{/* 用户名 */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="username">{commonT("username")}</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={e => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 密码 */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Label htmlFor="password">{commonT("password")}</Label>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={e => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* 邮箱 */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="email">{commonT("email")}</Label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="text"
|
||||||
|
placeholder="example@liteyuki.org"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={e => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSendVerifyCode} disabled={!email} variant="outline" className="border-2" type="button">
|
||||||
|
{commonT("send_verify_code")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 邮箱验证码 */}
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<Label htmlFor="email">{commonT("verify_code")}</Label>
|
||||||
|
<InputOTPControlled
|
||||||
|
onChange={value => setVerifyCode(value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{captchaProps &&
|
||||||
|
<div className="flex justify-center items-center w-full">
|
||||||
|
<Captcha {...captchaProps} onSuccess={setCaptchaToken} onError={handleCaptchaError} key={refreshCaptchaKey} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleRegister}
|
||||||
|
disabled={!captchaToken || isLogging || !username || !password || !email}
|
||||||
|
>
|
||||||
|
{isLogging ? t("registering") : t("register")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -10,15 +10,13 @@ import {
|
|||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import Image from "next/image"
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { requestEmailVerifyCode, resetPassword } from "@/api/user"
|
import { requestEmailVerifyCode, resetPassword } from "@/api/user"
|
||||||
import Link from "next/link"
|
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { InputOTPControlled } from "@/components/common/input-otp"
|
import { InputOTPControlled } from "@/components/common/input-otp"
|
||||||
import { BaseErrorResponse } from "@/models/resp"
|
import { BaseErrorResponse } from "@/models/resp"
|
||||||
import { loginPath, useToLogin } from "@/hooks/use-route"
|
import { loginPath } from "@/hooks/use-route"
|
||||||
import router from "next/router"
|
import router from "next/router"
|
||||||
|
|
||||||
export function ResetPasswordForm({
|
export function ResetPasswordForm({
|
||||||
@ -26,7 +24,7 @@ export function ResetPasswordForm({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div">) {
|
}: React.ComponentProps<"div">) {
|
||||||
const t = useTranslations('ResetPassword')
|
const t = useTranslations('ResetPassword')
|
||||||
const toLogin = useToLogin();
|
const commonT = useTranslations('Common')
|
||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [verifyCode, setVerifyCode] = useState("")
|
const [verifyCode, setVerifyCode] = useState("")
|
||||||
const [newPassword, setNewPassword] = useState("")
|
const [newPassword, setNewPassword] = useState("")
|
||||||
@ -65,7 +63,7 @@ export function ResetPasswordForm({
|
|||||||
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<Label htmlFor="email">{t("email")}</Label>
|
<Label htmlFor="email">{commonT("email")}</Label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
<Button
|
<Button
|
15
web/src/components/common/section-divider.tsx
Normal file
15
web/src/components/common/section-divider.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function SectionDivider({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
cn("after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t",
|
||||||
|
className)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="bg-card text-muted-foreground relative z-10 px-2" {...props}>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,14 +9,15 @@ import { toast } from "sonner";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { BaseErrorResponse } from "@/models/resp";
|
import { BaseErrorResponse } from "@/models/resp";
|
||||||
import { useAuth } from "@/contexts/auth-context";
|
import { useAuth } from "@/contexts/auth-context";
|
||||||
import { useToResetPassword } from "@/hooks/use-route";
|
import { resetPasswordPath } from "@/hooks/use-route";
|
||||||
import { InputOTPControlled } from "@/components/common/input-otp";
|
import { InputOTPControlled } from "@/components/common/input-otp";
|
||||||
|
import Link from "next/link";
|
||||||
// const VERIFY_CODE_COOL_DOWN = 60; // seconds
|
// const VERIFY_CODE_COOL_DOWN = 60; // seconds
|
||||||
|
|
||||||
export function UserSecurityPage() {
|
export function UserSecurityPage() {
|
||||||
const t = useTranslations("Console.user_security")
|
const t = useTranslations("Console.user_security")
|
||||||
|
const commonT = useTranslations("Common")
|
||||||
const { user, setUser } = useAuth();
|
const { user, setUser } = useAuth();
|
||||||
const toResetPassword = useToResetPassword();
|
|
||||||
const [email, setEmail] = useState(user?.email || "")
|
const [email, setEmail] = useState(user?.email || "")
|
||||||
const [verifyCode, setVerifyCode] = useState("")
|
const [verifyCode, setVerifyCode] = useState("")
|
||||||
const [oldPassword, setOldPassword] = useState("")
|
const [oldPassword, setOldPassword] = useState("")
|
||||||
@ -70,7 +71,7 @@ export function UserSecurityPage() {
|
|||||||
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
<Input id="password" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} />
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<Button disabled={!oldPassword || !newPassword} className="max-w-1/3 border-2" onClick={handleSubmitPassword}>{t("update_password")}</Button>
|
<Button disabled={!oldPassword || !newPassword} className="max-w-1/3 border-2" onClick={handleSubmitPassword}>{t("update_password")}</Button>
|
||||||
<Button onClick={toResetPassword} variant="ghost">{t("forgot_password_or_no_password")}</Button>
|
<Link href={resetPasswordPath}>{t("forgot_password_or_no_password")}</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +80,7 @@ export function UserSecurityPage() {
|
|||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
{t("email_setting")}
|
{t("email_setting")}
|
||||||
</h1>
|
</h1>
|
||||||
<Label htmlFor="email">{t("email")}</Label>
|
<Label htmlFor="email">{commonT("email")}</Label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
<Button disabled={!email || email == user.email} variant="outline" className="border-2" onClick={handleSendVerifyCode}>{t("send_verify_code")}</Button>
|
<Button disabled={!email || email == user.email} variant="outline" className="border-2" onClick={handleSendVerifyCode}>{t("send_verify_code")}</Button>
|
||||||
|
@ -31,6 +31,7 @@ export function AvatarWithDropdownMenu() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<button className="rounded-full overflow-hidden">
|
<button className="rounded-full overflow-hidden">
|
||||||
{user ? <Avatar className="h-8 w-8 rounded-full">
|
{user ? <Avatar className="h-8 w-8 rounded-full">
|
||||||
@ -39,26 +40,33 @@ export function AvatarWithDropdownMenu() {
|
|||||||
</Avatar> : <CircleUser className="h-8 w-8" />}
|
</Avatar> : <CircleUser className="h-8 w-8" />}
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent className="w-auto" align="start">
|
<DropdownMenuContent className="w-auto" align="start">
|
||||||
<DropdownMenuLabel>
|
{user && <DropdownMenuLabel>
|
||||||
{user && <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||||
<span className="truncate font-medium">{formatDisplayName(user)}</span>
|
<span className="truncate font-medium">{formatDisplayName(user)}</span>
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
<span className="text-muted-foreground truncate text-xs">
|
||||||
{user.email}
|
{user.email}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>}
|
||||||
|
|
||||||
|
{user &&
|
||||||
|
<>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
{user && <DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`/u/${user?.username}`}>Profile</Link>
|
<Link href={`/u/${user?.username}`}>Profile</Link>
|
||||||
</DropdownMenuItem>}
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/console">Console</Link>
|
<Link href="/console">Console</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
<DropdownMenuItem onClick={user ? handleLogout : toLogin}>
|
<DropdownMenuItem onClick={user ? handleLogout : toLogin}>
|
||||||
{user ? "Logout" : "Login"}
|
{user ? "Logout" : "Login"}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
@ -17,8 +17,8 @@ import config from "@/config"
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
|
||||||
import { Menu } from "lucide-react"
|
import { Menu } from "lucide-react"
|
||||||
import { ThemeModeToggle } from "../common/theme-toggle"
|
import { ThemeModeToggle } from "@/components/common/theme-toggle"
|
||||||
import { AvatarWithDropdownMenu } from "./avatar-with-dropdown-menu"
|
import { AvatarWithDropdownMenu } from "@/components/layout/nav/avatar-with-dropdown-menu"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const navbarMenuComponents = [
|
const navbarMenuComponents = [
|
@ -5,8 +5,10 @@ import { useRouter, usePathname } from "next/navigation"
|
|||||||
* 用于跳转到登录页并自动带上 redirect_back 参数
|
* 用于跳转到登录页并自动带上 redirect_back 参数
|
||||||
* 用法:const toLogin = useToLogin(); <Button onClick={toLogin}>去登录</Button>
|
* 用法:const toLogin = useToLogin(); <Button onClick={toLogin}>去登录</Button>
|
||||||
*/
|
*/
|
||||||
export const loginPath = "/login"
|
export const authPath = "/auth"
|
||||||
export const resetPasswordPath = "/reset-password"
|
export const loginPath = authPath + "/login"
|
||||||
|
export const registerPath = authPath + "/register"
|
||||||
|
export const resetPasswordPath = authPath + "/reset-password"
|
||||||
|
|
||||||
export function useToLogin() {
|
export function useToLogin() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -16,14 +18,6 @@ export function useToLogin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useToResetPassword() {
|
|
||||||
const router = useRouter()
|
|
||||||
const pathname = usePathname()
|
|
||||||
return () => {
|
|
||||||
router.push(`${resetPasswordPath}?redirect_back=${encodeURIComponent(pathname)}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useToUserProfile() {
|
export function useToUserProfile() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (username: string) => {
|
return (username: string) => {
|
||||||
|
@ -54,13 +54,19 @@
|
|||||||
"update": "更新"
|
"update": "更新"
|
||||||
},
|
},
|
||||||
"Common": {
|
"Common": {
|
||||||
|
"email": "邮箱",
|
||||||
|
"forgot_password": "忘记密码?",
|
||||||
"login": "登录",
|
"login": "登录",
|
||||||
"daysAgo": "天前",
|
"daysAgo": "天前",
|
||||||
"hoursAgo": "小时前",
|
"hoursAgo": "小时前",
|
||||||
"minutesAgo": "分钟前",
|
"minutesAgo": "分钟前",
|
||||||
|
"password": "密码",
|
||||||
"secondsAgo": "秒前",
|
"secondsAgo": "秒前",
|
||||||
|
"send_verify_code": "发送验证码",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"update": "更新"
|
"update": "更新",
|
||||||
|
"username": "用户名",
|
||||||
|
"verify_code": "验证码"
|
||||||
},
|
},
|
||||||
"Console": {
|
"Console": {
|
||||||
"comment": {
|
"comment": {
|
||||||
@ -101,7 +107,6 @@
|
|||||||
},
|
},
|
||||||
"user_security": {
|
"user_security": {
|
||||||
"title": "安全设置",
|
"title": "安全设置",
|
||||||
"email": "邮箱",
|
|
||||||
"email_setting": "邮箱设置",
|
"email_setting": "邮箱设置",
|
||||||
"forgot_password_or_no_password": "忘记密码或没有密码",
|
"forgot_password_or_no_password": "忘记密码或没有密码",
|
||||||
"new_password": "新密码",
|
"new_password": "新密码",
|
||||||
@ -124,6 +129,8 @@
|
|||||||
},
|
},
|
||||||
"Login": {
|
"Login": {
|
||||||
"captcha_error": "验证错误,请重试。",
|
"captcha_error": "验证错误,请重试。",
|
||||||
|
"continue": "继续",
|
||||||
|
"currently_logged_in": "当前已登录为",
|
||||||
"fetch_captcha_config_failed": "获取验证码失败,请稍后重试。",
|
"fetch_captcha_config_failed": "获取验证码失败,请稍后重试。",
|
||||||
"fetch_oidc_configs_failed": "获取第三方身份提供者配置失败。",
|
"fetch_oidc_configs_failed": "获取第三方身份提供者配置失败。",
|
||||||
"logging": "正在登录...",
|
"logging": "正在登录...",
|
||||||
@ -143,9 +150,16 @@
|
|||||||
"and": "和",
|
"and": "和",
|
||||||
"privacy_policy": "隐私政策"
|
"privacy_policy": "隐私政策"
|
||||||
},
|
},
|
||||||
|
"Register": {
|
||||||
|
"title": "注册",
|
||||||
|
"register": "注册",
|
||||||
|
"registering": "注册中...",
|
||||||
|
"register_a_new_account": "注册一个新账号",
|
||||||
|
"register_failed": "注册失败",
|
||||||
|
"register_success": "注册成功!"
|
||||||
|
},
|
||||||
"ResetPassword": {
|
"ResetPassword": {
|
||||||
"title": "重置密码",
|
"title": "重置密码",
|
||||||
"email": "邮箱",
|
|
||||||
"new_password": "新密码",
|
"new_password": "新密码",
|
||||||
"reset_password": "重置密码",
|
"reset_password": "重置密码",
|
||||||
"reset_password_failed": "重置密码失败",
|
"reset_password_failed": "重置密码失败",
|
||||||
|
@ -14,11 +14,3 @@ export enum Role {
|
|||||||
USER = "user",
|
USER = "user",
|
||||||
EDITOR = "editor",
|
EDITOR = "editor",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterRequest {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
nickname: string
|
|
||||||
email: string
|
|
||||||
verificationCode?: string
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user