mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
feat: 更新用户界面,添加注销功能和绑定第三方账号选项,优化表单布局
This commit is contained in:
@ -1,100 +1,100 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type UserDto struct {
|
type UserDto struct {
|
||||||
ID uint `json:"id"` // 用户ID
|
ID uint `json:"id"` // 用户ID
|
||||||
Username string `json:"username"` // 用户名
|
Username string `json:"username"` // 用户名
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
AvatarUrl string `json:"avatar_url"` // 头像URL
|
AvatarUrl string `json:"avatar_url"` // 头像URL
|
||||||
Email string `json:"email"` // 邮箱
|
Email string `json:"email"` // 邮箱
|
||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Language string `json:"language"` // 语言
|
Language string `json:"language"` // 语言
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserOidcConfigDto struct {
|
type UserOidcConfigDto struct {
|
||||||
Name string `json:"name"` // OIDC配置名称
|
Name string `json:"name"` // OIDC配置名称
|
||||||
DisplayName string `json:"display_name"` // OIDC配置显示名称
|
DisplayName string `json:"display_name"` // OIDC配置显示名称
|
||||||
Icon string `json:"icon"` // OIDC配置图标URL
|
Icon string `json:"icon"` // OIDC配置图标URL
|
||||||
LoginUrl string `json:"login_url"` // OIDC登录URL
|
LoginUrl string `json:"login_url"` // OIDC登录URL
|
||||||
}
|
}
|
||||||
type UserLoginReq struct {
|
type UserLoginReq struct {
|
||||||
Username string `json:"username"` // username or email
|
Username string `json:"username"` // username or email
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserLoginResp struct {
|
type UserLoginResp struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
User UserDto `json:"user"`
|
User UserDto `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegisterReq struct {
|
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:"-" binding:"-"`
|
Email string `json:"-" binding:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserRegisterResp struct {
|
type UserRegisterResp struct {
|
||||||
Token string `json:"token"` // 访问令牌
|
Token string `json:"token"` // 访问令牌
|
||||||
RefreshToken string `json:"refresh_token"` // 刷新令牌
|
RefreshToken string `json:"refresh_token"` // 刷新令牌
|
||||||
User UserDto `json:"user"` // 用户信息
|
User UserDto `json:"user"` // 用户信息
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyEmailReq struct {
|
type VerifyEmailReq struct {
|
||||||
Email string `json:"email"` // 邮箱地址
|
Email string `json:"email"` // 邮箱地址
|
||||||
}
|
}
|
||||||
|
|
||||||
type VerifyEmailResp struct {
|
type VerifyEmailResp struct {
|
||||||
Success bool `json:"success"` // 验证码发送成功与否
|
Success bool `json:"success"` // 验证码发送成功与否
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcLoginReq struct {
|
type OidcLoginReq struct {
|
||||||
Name string `json:"name"` // OIDC配置名称
|
Name string `json:"name"` // OIDC配置名称
|
||||||
Code string `json:"code"` // OIDC授权码
|
Code string `json:"code"` // OIDC授权码
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcLoginResp struct {
|
type OidcLoginResp struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
User UserDto `json:"user"`
|
User UserDto `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListOidcConfigResp struct {
|
type ListOidcConfigResp struct {
|
||||||
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
|
OidcConfigs []UserOidcConfigDto `json:"oidc_configs"` // OIDC配置列表
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUserReq struct {
|
type GetUserReq struct {
|
||||||
UserID uint `json:"user_id"`
|
UserID uint `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUserByUsernameReq struct {
|
type GetUserByUsernameReq struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUserResp struct {
|
type GetUserResp struct {
|
||||||
User UserDto `json:"user"` // 用户信息
|
User UserDto `json:"user"` // 用户信息
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserReq struct {
|
type UpdateUserReq struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
AvatarUrl string `json:"avatar_url"`
|
AvatarUrl string `json:"avatar_url"`
|
||||||
Gender string `json:"gender"`
|
Gender string `json:"gender"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserResp struct {
|
type UpdateUserResp struct {
|
||||||
User *UserDto `json:"user"` // 更新后的用户信息
|
User *UserDto `json:"user"` // 更新后的用户信息
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdatePasswordReq struct {
|
type UpdatePasswordReq struct {
|
||||||
OldPassword string `json:"old_password"`
|
OldPassword string `json:"old_password"`
|
||||||
NewPassword string `json:"new_password"`
|
NewPassword string `json:"new_password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetPasswordReq struct {
|
type ResetPasswordReq struct {
|
||||||
Email string `json:"-" binding:"-"`
|
Email string `json:"-" binding:"-"`
|
||||||
NewPassword string `json:"new_password"`
|
NewPassword string `json:"new_password"`
|
||||||
}
|
}
|
||||||
|
@ -8,33 +8,49 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SectionDivider } from '@/components/common/section-divider';
|
import { SectionDivider } from '@/components/common/section-divider';
|
||||||
|
import { LogOut } from "lucide-react";
|
||||||
|
import { userLogout } from "@/api/user";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function CurrentLogged() {
|
export function CurrentLogged() {
|
||||||
const t = useTranslations("Login");
|
const t = useTranslations("Login");
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const redirectBack = searchParams.get("redirect_back") || "/"
|
const redirectBack = searchParams.get("redirect_back") || "/"
|
||||||
const { user } = useAuth();
|
const { user, logout } = useAuth();
|
||||||
|
|
||||||
const handleLoggedContinue = () => {
|
const handleLoggedContinue = () => {
|
||||||
router.push(redirectBack);
|
router.push(redirectBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogOut = () => {
|
||||||
|
userLogout().then(() => {
|
||||||
|
logout();
|
||||||
|
toast.success(t("logout_success"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="mb-4">
|
||||||
<SectionDivider className="mb-4">{t("currently_logged_in")}</SectionDivider>
|
<SectionDivider className="mb-4">{t("currently_logged_in")}</SectionDivider>
|
||||||
<div onClick={handleLoggedContinue} className="cursor-pointer">
|
<div className="flex justify-evenly items-center">
|
||||||
<div className="flex gap-2 justify-center items-center">
|
<div className="flex gap-4 items-center cursor-pointer">
|
||||||
<Avatar className="h-10 w-10 rounded-full">
|
<div onClick={handleLoggedContinue} className="flex gap-2 justify-center items-center ">
|
||||||
<AvatarImage src={getGravatarFromUser({ user })} alt={user.username} />
|
<Avatar className="h-10 w-10 rounded-full">
|
||||||
<AvatarFallback className="rounded-full">{getFallbackAvatarFromUsername(user.nickname || user.username)}</AvatarFallback>
|
<AvatarImage src={getGravatarFromUser({ user })} alt={user.username} />
|
||||||
</Avatar>
|
<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>
|
||||||
<div className="grid place-items-center text-sm leading-tight text-center">
|
<div>
|
||||||
<span className="text-primary font-medium">{formatDisplayName(user)}</span>
|
<LogOut onClick={handleLogOut} className="text-muted-foreground cursor-pointer" />
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -25,6 +25,7 @@ import { useAuth } from "@/contexts/auth-context"
|
|||||||
import { registerPath, resetPasswordPath } from "@/hooks/use-route"
|
import { registerPath, resetPasswordPath } from "@/hooks/use-route"
|
||||||
import { CurrentLogged } from "@/components/auth/common/current-logged"
|
import { CurrentLogged } from "@/components/auth/common/current-logged"
|
||||||
import { SectionDivider } from "@/components/common/section-divider"
|
import { SectionDivider } from "@/components/common/section-divider"
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
|
|
||||||
export function LoginForm({
|
export function LoginForm({
|
||||||
className,
|
className,
|
||||||
@ -103,9 +104,9 @@ export function LoginForm({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<CurrentLogged />
|
<CurrentLogged />
|
||||||
<SectionDivider className="my-4">{t("with_oidc")}</SectionDivider>
|
<SectionDivider className="mb-6">{user ? t("bind_oidc") : t("with_oidc")}</SectionDivider>
|
||||||
<form>
|
<form>
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-4">
|
||||||
{/* OIDC 登录选项 */}
|
{/* OIDC 登录选项 */}
|
||||||
{oidcConfigs.length > 0 && (
|
{oidcConfigs.length > 0 && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@ -130,10 +131,10 @@ export function LoginForm({
|
|||||||
)}
|
)}
|
||||||
{/* 分隔线 */}
|
{/* 分隔线 */}
|
||||||
{oidcConfigs.length > 0 && (
|
{oidcConfigs.length > 0 && (
|
||||||
<SectionDivider className="my-0"> {t("or_continue_with_local_account")}</SectionDivider>
|
<SectionDivider className="my-2"> {t("or_continue_with_local_account")}</SectionDivider>
|
||||||
)}
|
)}
|
||||||
{/* 邮箱密码登录 */}
|
{/* 邮箱密码登录 */}
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<Label htmlFor="email">{t("email_or_username")}</Label>
|
<Label htmlFor="email">{t("email_or_username")}</Label>
|
||||||
<Input
|
<Input
|
||||||
@ -181,7 +182,7 @@ export function LoginForm({
|
|||||||
{/* 注册链接 */}
|
{/* 注册链接 */}
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
{t("no_account")}{" "}
|
{t("no_account")}{" "}
|
||||||
<Link href={registerPath+"?redirect_back="+encodeURIComponent(redirectBack)} 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>
|
||||||
@ -211,20 +212,10 @@ function LoginWithOidc({
|
|||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href={loginUrl}>
|
<Link href={loginUrl}>
|
||||||
<Image
|
<Avatar className="h-6 w-6 rounded-full">
|
||||||
src={icon}
|
<AvatarImage src={icon} alt={displayName} />
|
||||||
alt={`${displayName} icon`}
|
<AvatarFallback className="rounded-full"></AvatarFallback>
|
||||||
width={16}
|
</Avatar>
|
||||||
height={16}
|
|
||||||
style={{
|
|
||||||
width: '16px',
|
|
||||||
height: '16px',
|
|
||||||
marginRight: '8px'
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{displayName}
|
{displayName}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -109,10 +109,10 @@ export function RegisterForm({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<CurrentLogged />
|
<CurrentLogged />
|
||||||
<form>
|
<form>
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-4">
|
||||||
<SectionDivider className="mt-4">{t("register_a_new_account")}</SectionDivider>
|
<SectionDivider className="mt-0">{t("register_a_new_account")}</SectionDivider>
|
||||||
|
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-4">
|
||||||
|
|
||||||
{/* 用户名 */}
|
{/* 用户名 */}
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
@ -152,17 +152,21 @@ export function RegisterForm({
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
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>
|
</div>
|
||||||
{/* 邮箱验证码 */}
|
{/* 邮箱验证码 */}
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<Label htmlFor="email">{commonT("verify_code")}</Label>
|
<Label htmlFor="email">{commonT("verify_code")}</Label>
|
||||||
<InputOTPControlled
|
<div className="flex justify-between">
|
||||||
onChange={value => setVerifyCode(value)}
|
<InputOTPControlled
|
||||||
/>
|
onChange={value => setVerifyCode(value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={handleSendVerifyCode} disabled={!email} variant="outline" className="border-2" type="button">
|
||||||
|
{commonT("send_verify_code")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{captchaProps &&
|
{captchaProps &&
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
|
@ -73,7 +73,7 @@ export function UserSecurityPage() {
|
|||||||
<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>
|
||||||
<Link href={resetPasswordPath}>{t("forgot_password_or_no_password")}</Link>
|
<Link href={resetPasswordPath}>{t("forgot_password_or_no_password")}</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
<div className="grid w-full max-w-sm items-center gap-3 py-4">
|
<div className="grid w-full max-w-sm items-center gap-3 py-4">
|
||||||
@ -83,13 +83,14 @@ export function UserSecurityPage() {
|
|||||||
<Label htmlFor="email">{commonT("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>
|
|
||||||
</div>
|
</div>
|
||||||
<Label htmlFor="verify-code">{t("verify_code")}</Label>
|
<Label htmlFor="verify-code">{t("verify_code")}</Label>
|
||||||
<div className="flex gap-3">
|
<div className="flex justify-between">
|
||||||
<InputOTPControlled onChange={(value) => setVerifyCode(value)} />
|
<InputOTPControlled onChange={(value) => setVerifyCode(value)} />
|
||||||
<Button disabled={verifyCode.length < 6} className="border-2" onClick={handleSubmitEmail}>{t("update_email")}</Button>
|
<Button disabled={!email || email == user.email} variant="outline" className="border-2" onClick={handleSendVerifyCode}>{t("send_verify_code")}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<Button disabled={verifyCode.length < 6} className="border-2" onClick={handleSubmitEmail}>{t("update_email")}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -128,6 +128,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Login": {
|
"Login": {
|
||||||
|
"bind_oidc": "绑定第三方账号",
|
||||||
"captcha_error": "验证错误,请重试。",
|
"captcha_error": "验证错误,请重试。",
|
||||||
"continue": "继续",
|
"continue": "继续",
|
||||||
"currently_logged_in": "当前已登录为",
|
"currently_logged_in": "当前已登录为",
|
||||||
@ -137,7 +138,7 @@
|
|||||||
"login_success": "登录成功!",
|
"login_success": "登录成功!",
|
||||||
"login_failed": "登录失败",
|
"login_failed": "登录失败",
|
||||||
"welcome": "欢迎回来",
|
"welcome": "欢迎回来",
|
||||||
"with_oidc": "使用第三方身份提供者",
|
"with_oidc": "使用第三方账号",
|
||||||
"or_continue_with_local_account": "或使用用户名和密码",
|
"or_continue_with_local_account": "或使用用户名和密码",
|
||||||
"email_or_username": "邮箱或用户名",
|
"email_or_username": "邮箱或用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
|
Reference in New Issue
Block a user