refactor: restructure authentication components and routes
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:
2025-09-23 02:21:03 +08:00
parent 0f7cbb385a
commit 349cf5a5b7
23 changed files with 380 additions and 112 deletions

13
TODO.md
View File

@ -1,12 +1,7 @@
# TODO List
## 主页模块
## 代办
- [ ]主页文章列表
- [ ]主页侧边栏卡片列表
## 评论模块
- [ ]评论列表
- [ ]评论输入框
- [ ]评论回复功能
- [ ] 给邮件验证码加冷却时间,前后端都加
- [ ] 优化评论区的时间显示
- [ ] 在当前有用户登录时使用OIDC会自动绑定

View File

@ -52,6 +52,12 @@ func (u *UserController) Register(ctx context.Context, c *app.RequestContext) {
resps.BadRequest(c, resps.ErrParamInvalid)
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)
if err != nil {
@ -97,7 +103,7 @@ func (u *UserController) OidcLogin(ctx context.Context, c *app.RequestContext) {
Code: code,
State: state,
}
resp, err := u.service.OidcLogin(oidcLoginReq)
resp, err := u.service.OidcLogin(ctx, oidcLoginReq)
if err != nil {
serviceErr := errs.AsServiceError(err)
resps.Custom(c, serviceErr.Code, serviceErr.Message, nil)

View File

@ -29,11 +29,10 @@ type UserLoginResp struct {
}
type UserRegisterReq struct {
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"email"` // 邮箱
VerificationCode string `json:"verification_code"` // 邮箱验证码
Username string `json:"username"` // 用户名
Nickname string `json:"nickname"` // 昵称
Password string `json:"password"` // 密码
Email string `json:"-" binding:"-"`
}
type UserRegisterResp struct {

View File

@ -2,6 +2,7 @@ package middleware
import (
"context"
"strings"
"github.com/cloudwego/hertz/pkg/app"
"github.com/snowykami/neo-blog/pkg/constant"
@ -12,8 +13,8 @@ import (
// UseEmailVerify 中间件函数,用于邮箱验证,使用前先调用请求发送邮件验证码函数
func UseEmailVerify() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
email := string(c.GetHeader(constant.HeaderKeyEmail))
verifyCode := string(c.GetHeader(constant.HeaderKeyVerifyCode))
email := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyEmail)))
verifyCode := strings.TrimSpace(string(c.GetHeader(constant.HeaderKeyVerifyCode)))
if !utils.Env.GetAsBool(constant.EnvKeyEnableEmailVerify, true) {
c.Next(ctx)
}

View File

@ -15,7 +15,7 @@ func registerUserRoutes(group *route.RouterGroup) {
{
userGroupWithoutAuthNeedsCaptcha.POST("/login", userController.Login)
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("/oidc/list", userController.OidcList)
userGroupWithoutAuth.GET("/oidc/login/:name", userController.OidcLogin)

View File

@ -56,16 +56,9 @@ func (s *UserService) UserLogin(req *dto.UserLoginReq) (*dto.UserLoginResp, erro
}
func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterResp, error) {
// 验证邮箱验证码
if !utils.Env.GetAsBool(constant.EnvKeyEnableRegister, true) {
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)
if err != nil {
@ -185,7 +178,7 @@ func (s *UserService) ListOidcConfigs() ([]dto.UserOidcConfigDto, error) {
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
kvStore := utils.KV.GetInstance()
storedName, ok := kvStore.Get(constant.KVKeyOidcState + req.State)

View File

@ -1,6 +1,6 @@
import type { OidcConfig } from '@/models/oidc-config'
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 axiosClient from './client'
@ -31,11 +31,18 @@ export async function userLogout(): Promise<BaseResponse<null>> {
}
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 }>> {
const res = await axiosClient.post<BaseResponse<{ token: string, user: User }>>(
'/user/register',
data,
{ username, password, },
{ headers: { 'X-Email': email, 'X-VerifyCode': verifyCode || '' , 'X-Captcha-Token': captchaToken || ''} },
)
return res.data
}

View File

@ -1,7 +1,7 @@
'use client'
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 Footer from '@/components/layout/footer'
import config from '@/config'

View File

@ -1,6 +1,6 @@
import { Suspense } from 'react'
import { LoginForm } from '@/components/login/login-form'
import { AuthHeader } from '@/components/common/auth-header'
import { LoginForm } from '@/components/auth/login/login-form'
import { AuthHeader } from '@/components/auth/common/auth-header'
function LoginPageContent() {
return (

View 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>
)
}

View File

@ -1,5 +1,5 @@
import { AuthHeader } from "@/components/common/auth-header";
import { ResetPasswordForm } from "@/components/reset-password/reset-password-form";
import { AuthHeader } from "@/components/auth/common/auth-header";
import { ResetPasswordForm } from "@/components/auth/reset-password/reset-password-form";
export default function Page() {
return (
<div className="bg-muted flex min-h-svh flex-col items-center justify-center gap-6 p-6 md:p-10">

View 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>
)
}

View File

@ -18,19 +18,20 @@ import { getCaptchaConfig, listOidcConfigs, userLogin } from "@/api/user"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { useTranslations } from "next-intl"
import Captcha from "../common/captcha"
import Captcha from "@/components/common/captcha"
import { CaptchaProvider } from "@/models/captcha"
import { toast } from "sonner"
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({
className,
...props
}: React.ComponentProps<"div">) {
const t = useTranslations('Login')
const toResetPassword = useToResetPassword();
const {user, setUser} = useAuth();
const { user, setUser } = useAuth();
const [oidcConfigs, setOidcConfigs] = useState<OidcConfig[]>([])
const [captchaProps, setCaptchaProps] = useState<{
provider: CaptchaProvider
@ -45,12 +46,6 @@ export function LoginForm({
const searchParams = useSearchParams()
const redirectBack = searchParams.get("redirect_back") || "/"
useEffect(() => {
if (user) {
router.push(redirectBack);
}
}, [user, router, redirectBack]);
useEffect(() => {
listOidcConfigs()
.then((res) => {
@ -105,11 +100,10 @@ export function LoginForm({
<Card>
<CardHeader className="text-center">
<CardTitle className="text-xl">{t("welcome")}</CardTitle>
<CardDescription>
{t("with_oidc")}
</CardDescription>
</CardHeader>
<CardContent>
<CurrentLogged />
<SectionDivider className="my-4">{t("with_oidc")}</SectionDivider>
<form>
<div className="grid gap-6">
{/* OIDC 登录选项 */}
@ -134,16 +128,10 @@ export function LoginForm({
})}
</div>
)}
{/* 分隔线 */}
{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">
<span className="bg-card text-muted-foreground relative z-10 px-2">
{t("or_continue_with_local_account")}
</span>
</div>
<SectionDivider className="my-0"> {t("or_continue_with_local_account")}</SectionDivider>
)}
{/* 邮箱密码登录 */}
<div className="grid gap-6">
<div className="grid gap-3">
@ -193,7 +181,7 @@ export function LoginForm({
{/* 注册链接 */}
<div className="text-center text-sm">
{t("no_account")}{" "}
<Link href="#" className="underline underline-offset-4">
<Link href={registerPath+"?redirect_back="+encodeURIComponent(redirectBack)} className="underline underline-offset-4">
{t("register")}
</Link>
</div>
@ -201,18 +189,6 @@ export function LoginForm({
</form>
</CardContent>
</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>
)
}

View 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>
)
}

View File

@ -10,15 +10,13 @@ import {
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import Image from "next/image"
import { useState } from "react"
import { requestEmailVerifyCode, resetPassword } from "@/api/user"
import Link from "next/link"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import { InputOTPControlled } from "@/components/common/input-otp"
import { BaseErrorResponse } from "@/models/resp"
import { loginPath, useToLogin } from "@/hooks/use-route"
import { loginPath } from "@/hooks/use-route"
import router from "next/router"
export function ResetPasswordForm({
@ -26,7 +24,7 @@ export function ResetPasswordForm({
...props
}: React.ComponentProps<"div">) {
const t = useTranslations('ResetPassword')
const toLogin = useToLogin();
const commonT = useTranslations('Common')
const [email, setEmail] = useState("")
const [verifyCode, setVerifyCode] = 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)} />
</div>
<div className="grid gap-3">
<Label htmlFor="email">{t("email")}</Label>
<Label htmlFor="email">{commonT("email")}</Label>
<div className="flex gap-3">
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<Button

View 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>
);
}

View File

@ -9,14 +9,15 @@ import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { BaseErrorResponse } from "@/models/resp";
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 Link from "next/link";
// const VERIFY_CODE_COOL_DOWN = 60; // seconds
export function UserSecurityPage() {
const t = useTranslations("Console.user_security")
const commonT = useTranslations("Common")
const { user, setUser } = useAuth();
const toResetPassword = useToResetPassword();
const [email, setEmail] = useState(user?.email || "")
const [verifyCode, setVerifyCode] = 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)} />
<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 onClick={toResetPassword} variant="ghost">{t("forgot_password_or_no_password")}</Button>
<Link href={resetPasswordPath}>{t("forgot_password_or_no_password")}</Link>
</div>
</div>
@ -79,7 +80,7 @@ export function UserSecurityPage() {
<h1 className="text-2xl font-bold">
{t("email_setting")}
</h1>
<Label htmlFor="email">{t("email")}</Label>
<Label htmlFor="email">{commonT("email")}</Label>
<div className="flex gap-3">
<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>

View File

@ -31,6 +31,7 @@ export function AvatarWithDropdownMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="rounded-full overflow-hidden">
{user ? <Avatar className="h-8 w-8 rounded-full">
@ -39,26 +40,33 @@ export function AvatarWithDropdownMenu() {
</Avatar> : <CircleUser className="h-8 w-8" />}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-auto" align="start">
<DropdownMenuLabel>
{user && <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
{user && <DropdownMenuLabel>
<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">
<span className="truncate font-medium">{formatDisplayName(user)}</span>
<span className="text-muted-foreground truncate text-xs">
{user.email}
</span>
</div>
</div>}
</DropdownMenuLabel>
<DropdownMenuGroup>
{user && <DropdownMenuItem asChild>
<Link href={`/u/${user?.username}`}>Profile</Link>
</DropdownMenuItem>}
<DropdownMenuItem asChild>
<Link href="/console">Console</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</div>
</DropdownMenuLabel>}
{user &&
<>
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link href={`/u/${user?.username}`}>Profile</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/console">Console</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
</>
}
<DropdownMenuItem onClick={user ? handleLogout : toLogin}>
{user ? "Logout" : "Login"}
</DropdownMenuItem>

View File

@ -17,8 +17,8 @@ import config from "@/config"
import { useState } from "react"
import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
import { Menu } from "lucide-react"
import { ThemeModeToggle } from "../common/theme-toggle"
import { AvatarWithDropdownMenu } from "./avatar-with-dropdown-menu"
import { ThemeModeToggle } from "@/components/common/theme-toggle"
import { AvatarWithDropdownMenu } from "@/components/layout/nav/avatar-with-dropdown-menu"
import { cn } from "@/lib/utils"
const navbarMenuComponents = [

View File

@ -5,8 +5,10 @@ import { useRouter, usePathname } from "next/navigation"
* 用于跳转到登录页并自动带上 redirect_back 参数
* 用法const toLogin = useToLogin(); <Button onClick={toLogin}>去登录</Button>
*/
export const loginPath = "/login"
export const resetPasswordPath = "/reset-password"
export const authPath = "/auth"
export const loginPath = authPath + "/login"
export const registerPath = authPath + "/register"
export const resetPasswordPath = authPath + "/reset-password"
export function useToLogin() {
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() {
const router = useRouter();
return (username: string) => {

View File

@ -54,13 +54,19 @@
"update": "更新"
},
"Common": {
"email": "邮箱",
"forgot_password": "忘记密码?",
"login": "登录",
"daysAgo": "天前",
"hoursAgo": "小时前",
"minutesAgo": "分钟前",
"password": "密码",
"secondsAgo": "秒前",
"send_verify_code": "发送验证码",
"submit": "提交",
"update": "更新"
"update": "更新",
"username": "用户名",
"verify_code": "验证码"
},
"Console": {
"comment": {
@ -101,7 +107,6 @@
},
"user_security": {
"title": "安全设置",
"email": "邮箱",
"email_setting": "邮箱设置",
"forgot_password_or_no_password": "忘记密码或没有密码",
"new_password": "新密码",
@ -124,6 +129,8 @@
},
"Login": {
"captcha_error": "验证错误,请重试。",
"continue": "继续",
"currently_logged_in": "当前已登录为",
"fetch_captcha_config_failed": "获取验证码失败,请稍后重试。",
"fetch_oidc_configs_failed": "获取第三方身份提供者配置失败。",
"logging": "正在登录...",
@ -143,9 +150,16 @@
"and": "和",
"privacy_policy": "隐私政策"
},
"Register": {
"title": "注册",
"register": "注册",
"registering": "注册中...",
"register_a_new_account": "注册一个新账号",
"register_failed": "注册失败",
"register_success": "注册成功!"
},
"ResetPassword": {
"title": "重置密码",
"email": "邮箱",
"new_password": "新密码",
"reset_password": "重置密码",
"reset_password_failed": "重置密码失败",

View File

@ -14,11 +14,3 @@ export enum Role {
USER = "user",
EDITOR = "editor",
}
export interface RegisterRequest {
username: string
password: string
nickname: string
email: string
verificationCode?: string
}