diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico deleted file mode 100644 index ee46155f..00000000 Binary files a/frontend/app/favicon.ico and /dev/null differ diff --git a/frontend/app/icon.tsx b/frontend/app/icon.tsx new file mode 100644 index 00000000..3dca5dcc --- /dev/null +++ b/frontend/app/icon.tsx @@ -0,0 +1,31 @@ +import {ImageResponse} from 'next/og' +import {WavesIcon} from 'lucide-react' + +export const size = { + width: 32, + height: 32, +} +export const contentType = 'image/png' + +export default function Icon() { + return new ImageResponse( + ( +
+ +
+ ), + { + ...size, + } + ) +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index d36262c5..8c02df74 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -6,6 +6,7 @@ import {BellRingProvider} from "@/contexts/bell-ring-context"; import {NotificationSettingsProvider} from "@/contexts/notification-settings-context"; import {UserProvider} from "@/contexts/user-context"; import {AppQueryProvider} from "@/components/providers/query-provider"; +import {SiteTitleUpdater} from "@/components/providers/title-updater"; import {RobotsMeta} from "@/components/layout/robots-meta"; import "./globals.css"; @@ -36,6 +37,7 @@ export default function RootLayout({ > + diff --git a/frontend/components/auth/auth-shell.tsx b/frontend/components/auth/auth-shell.tsx new file mode 100644 index 00000000..cfbdb3b5 --- /dev/null +++ b/frontend/components/auth/auth-shell.tsx @@ -0,0 +1,67 @@ +"use client" + +import type {ReactNode} from "react" +import {motion} from "motion/react" +import {WavesIcon} from "lucide-react" + +import {cn} from "@/lib/utils" + +interface AuthShellProps { + children: ReactNode + wide?: boolean +} + +export function AuthShell({children, wide = false}: AuthShellProps) { + return ( +
+
+ ) +} + +interface AuthHeadingProps { + title: string + description: string + siteName?: string +} + +export function AuthHeading({ + title, + description, + siteName = "Wavelet", +}: AuthHeadingProps) { + return ( +
+
+ + + {siteName} +
+
+

+ {title} +

+

{description}

+
+
+ ) +} diff --git a/frontend/components/auth/login-form.tsx b/frontend/components/auth/login-form.tsx index 113c0722..41a06c7e 100644 --- a/frontend/components/auth/login-form.tsx +++ b/frontend/components/auth/login-form.tsx @@ -3,28 +3,31 @@ import {useEffect, useMemo, useRef, useState} from "react" import {useMutation, useQuery} from "@tanstack/react-query" import {useRouter, useSearchParams} from "next/navigation" -import {KeyRound} from "lucide-react" +import {EyeIcon, EyeOffIcon} from "lucide-react" import {toast} from "sonner" import Link from "next/link" import {useAuth} from "@/components/providers/auth-provider" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" -import {Label} from "@/components/ui/label" import {Separator} from "@/components/ui/separator" import {Spinner} from "@/components/ui/spinner" -import {Card, CardContent} from "@/components/ui/card" +import {Field, FieldGroup, FieldLabel} from "@/components/ui/field" import {CapWidget} from "@/components/auth/cap-widget" +import {AuthHeading} from "@/components/auth/auth-shell" import {OTPForm} from "./otp-form" import services from "@/lib/services" import type {LoginRequest} from "@/lib/services/auth/types" function getRedirectTarget(searchParams: ReturnType) { const callbackUrl = searchParams.get("callbackUrl") - const storedRedirect = sessionStorage.getItem("redirect_after_login") + const storedRedirect = + typeof window === "undefined" + ? null + : sessionStorage.getItem("redirect_after_login") const target = callbackUrl || storedRedirect || "/home" - if (storedRedirect) { + if (storedRedirect && typeof window !== "undefined") { sessionStorage.removeItem("redirect_after_login") } @@ -33,7 +36,7 @@ function getRedirectTarget(searchParams: ReturnType) { function persistRedirectTarget(searchParams: ReturnType) { const callbackUrl = searchParams.get("callbackUrl") - if (callbackUrl) { + if (callbackUrl && typeof window !== "undefined") { sessionStorage.setItem("redirect_after_login", callbackUrl) } } @@ -43,18 +46,23 @@ function configBool(value: string | undefined, fallback: boolean) { return value === "true" } -export function LoginForm() { +export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: boolean) => void }) { const router = useRouter() const searchParams = useSearchParams() const { setUser } = useAuth() const [username, setUsername] = useState("") const [password, setPassword] = useState("") + const [showPassword, setShowPassword] = useState(false) const [code, setCode] = useState("") const [showLoginCodeInput, setShowLoginCodeInput] = useState(false) const [loginCooldown, setLoginCooldown] = useState(0) const [errorMessage, setErrorMessage] = useState("") const [loginCodeTip, setLoginCodeTip] = useState(null) + useEffect(() => { + onOTPStateChange?.(showLoginCodeInput) + }, [showLoginCodeInput, onOTPStateChange]) + useEffect(() => { if (loginCooldown > 0) { const timer = setTimeout(() => setLoginCooldown(loginCooldown - 1), 1000) @@ -125,9 +133,7 @@ export function LoginForm() { if (errorMsg.startsWith("smtp_invalid:")) { const tip = errorMsg.substring("smtp_invalid:".length) - setLoginCodeTip( - {tip} - ) + setLoginCodeTip("请输入您的登录验证码。") setShowLoginCodeInput(true) setLoginCooldown(0) toast.warning(tip) @@ -217,11 +223,9 @@ export function LoginForm() { if (publicConfigQuery.isPending) { return ( - - - - - +
+ +
) } @@ -240,111 +244,123 @@ export function LoginForm() { } return ( - - -
-

- 账号登录 -

-
+
+ -
-
-
-
- - setUsername(e.target.value)} - placeholder="请输入邮箱或用户名" - autoComplete="username" - onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} - /> -
-
- - setPassword(e.target.value)} - type="password" - placeholder="请输入密码" - autoComplete="current-password" - onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} - /> -
- -
- - {/* Cap 人机验证 */} - {capEnabled && ( - + + + + 邮箱或用户名 * + + setUsername(e.target.value)} + placeholder="输入邮箱或用户名" + autoComplete="username" + className="h-10 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} + /> + + + + 密码 * + +
+ setPassword(e.target.value)} + type={showPassword ? "text" : "password"} + placeholder="输入您的密码" + autoComplete="current-password" + className="h-10 pr-11 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handlePasswordLogin()} /> - )} - - {errorMessage ? ( -
- {errorMessage} -
- ) : null} - - -
-
- - {authSources.length > 0 ? ( - <> - -
-
- {authSources.map((source) => ( - - ))} -
+
- + + + + {/* Cap 人机验证 */} + {capEnabled && ( + + )} + + {errorMessage ? ( +
+ {errorMessage} +
) : null} - {registrationEnabled && ( -
- {"Don't have an account?"}{" "} - - Sign up - + +
+ + {authSources.length > 0 ? ( +
+
+ + 或者使用 +
- )} - - +
+ {authSources.map((source) => ( + + ))} +
+
+ ) : null} + + {registrationEnabled && ( +
+ 还没有账号?{" "} + + 立即注册 + +
+ )} +
) } diff --git a/frontend/components/auth/login-page.tsx b/frontend/components/auth/login-page.tsx index b4cb0dd9..071af845 100644 --- a/frontend/components/auth/login-page.tsx +++ b/frontend/components/auth/login-page.tsx @@ -6,9 +6,9 @@ import {useRouter, useSearchParams} from "next/navigation" import {toast} from "sonner" import {Spinner} from "@/components/ui/spinner" import {LoginForm} from "@/components/auth/login-form" +import {AuthShell} from "@/components/auth/auth-shell" import {Check} from "lucide-react" -import {AuroraBackground} from "@/components/ui/aurora-background" import services from "@/lib/services" import {useAuth} from "@/components/providers/auth-provider" @@ -27,6 +27,7 @@ export function LoginPage() { const router = useRouter() const searchParams = useSearchParams() const { setUser } = useAuth() + const [showOTP, setShowOTP] = useState(false) /* 处理OAuth回调 */ const [isProcessingCallback, setIsProcessingCallback] = useState(() => { @@ -136,25 +137,8 @@ export function LoginPage() { }, [searchParams, router, resolveRedirectTarget, setUser]) return ( - - - {/*Title Do Not Remove*/} - {/*
*/} - {/*

*/} - {/* Refresh*/} - {/*

*/} - {/*
*/} - + +
{isProcessingCallback || isCheckingSession ? ( {isCheckingSession ? ( -
+
- +
-
+

正在检查登录状态

请稍候,我们正在确认当前会话...

) : loginSuccess ? ( -
+
- + -
+

登录成功

正在跳转至控制台...

) : ( -
+
- +
-
+

正在验证凭据

请稍候,我们正在为您建立安全会话...

@@ -209,11 +193,11 @@ export function LoginPage() { transition={{ duration: 0.4 }} className="w-full" > - + )} - - +
+ ) } diff --git a/frontend/components/auth/otp-form.tsx b/frontend/components/auth/otp-form.tsx index 358722ef..b50f6786 100644 --- a/frontend/components/auth/otp-form.tsx +++ b/frontend/components/auth/otp-form.tsx @@ -4,9 +4,9 @@ import * as React from "react" import {RefreshCwIcon} from "lucide-react" import {Button} from "@/components/ui/button" -import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card" import {Field, FieldLabel} from "@/components/ui/field" import {InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot,} from "@/components/ui/input-otp" +import {AuthHeading} from "@/components/auth/auth-shell" import {cn} from "@/lib/utils" interface OTPFormProps { @@ -29,17 +29,16 @@ export function OTPForm({ onSubmit, }: OTPFormProps) { return ( - - - - 验证登录 - - - {loginCodeTip} - - - - +
+ + {loginCodeTip ? ( +

{loginCodeTip}

+ ) : null} +
+
验证码 @@ -50,13 +49,13 @@ export function OTPForm({ type="button" onClick={onResend} disabled={loginCooldown > 0 || isPending} - className="gap-1 text-xs px-2 py-1 h-7" + className="h-8 text-xs" > - + {loginCooldown > 0 ? `${loginCooldown}秒后重发` : "重新发送"}
-
+
- - -
+
遇到登录问题?{" "}
- - +
+
) } diff --git a/frontend/components/auth/register-form.tsx b/frontend/components/auth/register-form.tsx index c7ea424e..c5093abf 100644 --- a/frontend/components/auth/register-form.tsx +++ b/frontend/components/auth/register-form.tsx @@ -3,22 +3,24 @@ import {useEffect, useMemo, useState} from "react" import {useMutation, useQuery} from "@tanstack/react-query" import {useRouter, useSearchParams} from "next/navigation" -import {UserPlus} from "lucide-react" import {toast} from "sonner" import Link from "next/link" import {useAuth} from "@/components/providers/auth-provider" import {Button} from "@/components/ui/button" import {Input} from "@/components/ui/input" -import {Label} from "@/components/ui/label" import {Spinner} from "@/components/ui/spinner" -import {Card, CardContent} from "@/components/ui/card" +import {Field, FieldGroup, FieldLabel} from "@/components/ui/field" +import {AuthHeading} from "@/components/auth/auth-shell" import services from "@/lib/services" import type {RegisterRequest} from "@/lib/services/auth/types" function getRedirectTarget(searchParams: ReturnType) { const callbackUrl = searchParams.get("callbackUrl") - const storedRedirect = sessionStorage.getItem("redirect_after_login") + const storedRedirect = + typeof window === "undefined" + ? null + : sessionStorage.getItem("redirect_after_login") return callbackUrl || storedRedirect || "/home" } @@ -142,11 +144,9 @@ export function RegisterForm() { if (publicConfigQuery.isPending) { return ( - - - - - +
+ +
) } @@ -155,125 +155,124 @@ export function RegisterForm() { } return ( - - -
-

- 创建账号 -

-
+
+ -
-
-
- - setUsername(e.target.value)} - placeholder="请输入用户名" - autoComplete="username" - onKeyDown={(e) => e.key === "Enter" && handleRegister()} - /> -
-
- - setNickname(e.target.value)} - placeholder="请输入昵称" - autoComplete="nickname" - onKeyDown={(e) => e.key === "Enter" && handleRegister()} - /> -
-
- - setPassword(e.target.value)} - type="password" - placeholder="请输入密码(至少 8 位)" - autoComplete="new-password" - onKeyDown={(e) => e.key === "Enter" && handleRegister()} - /> -
-
- - setEmail(e.target.value)} - placeholder="请输入电子邮箱" - autoComplete="email" - onKeyDown={(e) => e.key === "Enter" && handleRegister()} - /> -
- {emailRegisterEnabled && ( -
- -
- setCode(e.target.value)} - placeholder="请输入 6 位邮箱验证码" - maxLength={6} - className="flex-1" - onKeyDown={(e) => e.key === "Enter" && handleRegister()} - /> - -
+
+ + + 用户名 * + setUsername(e.target.value)} + placeholder="请输入用户名" + autoComplete="username" + className="h-10 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + + + + 昵称 + (可选) + + setNickname(e.target.value)} + placeholder="请输入昵称" + autoComplete="nickname" + className="h-10 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + + + 电子邮箱 * + setEmail(e.target.value)} + placeholder="请输入电子邮箱" + autoComplete="email" + className="h-10 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + + + 密码 * + setPassword(e.target.value)} + type="password" + placeholder="请输入密码(至少 8 位)" + autoComplete="new-password" + className="h-10 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> + + {emailRegisterEnabled && ( + + 邮箱验证码 * +
+ setCode(e.target.value)} + placeholder="请输入 6 位邮箱验证码" + maxLength={6} + className="h-10 flex-1 text-sm [@media(max-height:700px)]:h-9" + onKeyDown={(e) => e.key === "Enter" && handleRegister()} + /> +
- )} +
+ )} +
+ + {errorMessage ? ( +
+ {errorMessage}
+ ) : null} - {errorMessage ? ( -
- {errorMessage} -
- ) : null} + +
- -
- -
- Already have an account?{" "} - - Sign in - -
- - +
+ 已经有账号?{" "} + + 返回登录 + +
+
) } diff --git a/frontend/components/auth/register-page.tsx b/frontend/components/auth/register-page.tsx index cba9b374..755bda44 100644 --- a/frontend/components/auth/register-page.tsx +++ b/frontend/components/auth/register-page.tsx @@ -1,25 +1,12 @@ "use client" -import {motion} from "motion/react" import {RegisterForm} from "@/components/auth/register-form" -import {AuroraBackground} from "@/components/ui/aurora-background" +import {AuthShell} from "@/components/auth/auth-shell" export function RegisterPage() { return ( - - - - - + + + ) } diff --git a/frontend/components/providers/title-updater.tsx b/frontend/components/providers/title-updater.tsx new file mode 100644 index 00000000..54281001 --- /dev/null +++ b/frontend/components/providers/title-updater.tsx @@ -0,0 +1,36 @@ +"use client" + +import {useQuery} from '@tanstack/react-query' +import {useEffect} from 'react' +import services from '@/lib/services' +import {usePathname} from 'next/navigation' + +export function SiteTitleUpdater() { + const pathname = usePathname() + const publicConfigQuery = useQuery({ + queryKey: ["public-config"], + queryFn: () => services.config.getPublicConfig(), + }) + + useEffect(() => { + const siteName = publicConfigQuery.data?.site_name || "Wavelet Platform" + + // Determine the page suffix based on path + let suffix = "" + if (pathname === "/login") { + suffix = " - 登录" + } else if (pathname === "/register") { + suffix = " - 注册" + } else if (pathname.startsWith("/admin")) { + suffix = " - 后台管理" + } else if (pathname === "/home") { + suffix = " - 控制台" + } else if (pathname === "/") { + suffix = "" + } + + document.title = `${siteName}${suffix}` + }, [publicConfigQuery.data?.site_name, pathname]) + + return null +} diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index 64ec63c8..b7826d93 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -1,8 +1,8 @@ import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import {Slot} from "@radix-ui/react-slot" +import {cva, type VariantProps} from "class-variance-authority" -import { cn } from "@/lib/utils" +import {cn} from "@/lib/utils" const buttonVariants = cva( "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -10,6 +10,7 @@ const buttonVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", + auth: "bg-foreground text-background hover:bg-foreground/90", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: diff --git a/frontend/proxy.ts b/frontend/proxy.ts index 4bb0d6a3..f692e4cd 100644 --- a/frontend/proxy.ts +++ b/frontend/proxy.ts @@ -103,7 +103,7 @@ export function proxy(request: NextRequest) { } /* 页面请求:公共路由放行 */ - const publicRoutes = ['/', '/login', '/callback', '/privacy', '/terms'] + const publicRoutes = ['/', '/login', '/register', '/callback', '/privacy', '/terms'] const publicPrefixes = ['/docs/', '/epay/'] if (publicRoutes.includes(pathname) || publicPrefixes.some(p => pathname.startsWith(p))) { diff --git a/internal/apps/user/errs.go b/internal/apps/user/errs.go index fabd3f71..c8ed94a0 100644 --- a/internal/apps/user/errs.go +++ b/internal/apps/user/errs.go @@ -5,43 +5,43 @@ package user const ( - errBindParamsFailed = "参数绑定失败" - errInvalidParams = "无效的参数" - errPasswordLoginDisabled = "管理员关闭了密码登录" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errUsernameOrPasswordWrong = "用户名或密码错误" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errLoginEmailMissing = "该账号未绑定邮箱,请联系管理员绑定邮箱后再登录" - errNeedEmailCodePrefix = "need_email_code:" - errSMTPInvalidUseTempCodePrefix = "smtp_invalid:" - errSMTPInvalidUseTempCode = "smtp 配置无效,使用临时码登录" - errEmailCodeInvalidOrExpired = "验证码错误或已过期" - errSaveSessionFailed = "无法保存会话信息,请重试" - errRegistrationDisabled = "管理员关闭了注册" - errPasswordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errEmailOrCodeRequired = "邮箱或验证码未填写" - errNewPasswordTooShort = "新密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errLoginRequired = "请先登录" - errUserNotFound = "未找到该用户" - errOldPasswordIncorrect = "原密码不正确" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errPasswordEncryptFailed = "密码加密失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errEmailRequired = "邮箱地址不能为空" - errUnsupportedEmailScene = "不支持的验证场景" - errEmailAlreadyRegistered = "该邮箱已被注册" - errEmailCodeCooldown = "验证码发送频繁,请稍后再试" - errEmailFormatInvalid = "邮箱格式不正确" - errEmailAlreadyBound = "该邮箱已被其他账号绑定" - errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w" - errGenerateEmailCodeFailed = "生成验证码失败,请重试" - errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试" - errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errAdminTokenRequiresAdmin = "只有管理员才能创建具有管理员权限的令牌" //nolint:gosec // false positive: this is an error message, not hardcoded credentials - errTaskPayloadRequired = "任务参数不能为空" - errInvalidJSONFormat = "无效的 JSON 格式: %w" - errEmailTaskFieldsRequired = "to、subject、body 不能为空" - errParseEmailPayloadFailed = "解析邮件发送参数失败: %w" - errSMTPConfigIncomplete = "系统 SMTP 邮件服务配置不完整" - errSendMailFailed = "发送邮件失败: %w" + errBindParamsFailed = "参数绑定失败" + errInvalidParams = "无效的参数" + errPasswordLoginDisabled = "管理员关闭了密码登录" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errUsernameOrPasswordWrong = "用户名或密码错误" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errLoginEmailMissing = "该账号未绑定邮箱,请联系管理员绑定邮箱后再登录" + errNeedEmailCodePrefix = "need_email_code:" + errSMTPInvalidUseTempCodePrefix = "smtp_invalid:" + errSMTPInvalidUseTempCode = "smtp 配置无效,使用临时码登录" + errEmailCodeInvalidOrExpired = "验证码错误或已过期" + errSaveSessionFailed = "无法保存会话信息,请重试" + errRegistrationDisabled = "管理员关闭了注册" + errPasswordTooShort = "密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errEmailOrCodeRequired = "邮箱或验证码未填写" + errNewPasswordTooShort = "新密码长度不能少于 8 位" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errLoginRequired = "请先登录" + errUserNotFound = "未找到该用户" + errOldPasswordIncorrect = "原密码不正确" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errPasswordEncryptFailed = "密码加密失败,请重试" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errEmailRequired = "邮箱地址不能为空" + errUnsupportedEmailScene = "不支持的验证场景" + errEmailAlreadyRegistered = "该邮箱已被注册" + errEmailCodeCooldown = "验证码发送频繁,请稍后再试" + errEmailFormatInvalid = "邮箱格式不正确" + errEmailAlreadyBound = "该邮箱已被其他账号绑定" + errRenderEmailTemplateFailed = "渲染验证邮件模板失败:%w" + errGenerateEmailCodeFailed = "生成验证码失败,请重试" + errDispatchEmailTaskFailed = "投递验证邮件发送任务失败,请重试" + errTokenNameRequired = "令牌名称不能为空" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errAccessTokenLimitReached = "已达到访问令牌最大创建数量限制" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errGenerateTokenFailed = "生成令牌失败" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errInvalidTokenID = "无效的令牌ID" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errTokenNotFoundOrForbidden = "令牌不存在或无权操作" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errAdminTokenRequiresAdmin = "只有管理员才能创建具有管理员权限的令牌" //nolint:gosec // false positive: this is an error message, not hardcoded credentials + errTaskPayloadRequired = "任务参数不能为空" + errInvalidJSONFormat = "无效的 JSON 格式: %w" + errEmailTaskFieldsRequired = "to、subject、body 不能为空" + errParseEmailPayloadFailed = "解析邮件发送参数失败: %w" + errSMTPConfigIncomplete = "系统 SMTP 邮件服务配置不完整" + errSendMailFailed = "发送邮件失败: %w" )