mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 02:56:22 +00:00
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/snowykami/neo-blog/pkg/constant"
|
||||||
"github.com/snowykami/neo-blog/pkg/resps"
|
"github.com/snowykami/neo-blog/pkg/resps"
|
||||||
"github.com/snowykami/neo-blog/pkg/utils"
|
"github.com/snowykami/neo-blog/pkg/utils"
|
||||||
)
|
)
|
||||||
@ -14,8 +15,8 @@ func UseCaptcha() app.HandlerFunc {
|
|||||||
captchaConfig := utils.Captcha.GetCaptchaConfigFromEnv()
|
captchaConfig := utils.Captcha.GetCaptchaConfigFromEnv()
|
||||||
return func(ctx context.Context, c *app.RequestContext) {
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
CaptchaToken := string(c.GetHeader("X-Captcha-Token"))
|
CaptchaToken := string(c.GetHeader("X-Captcha-Token"))
|
||||||
if utils.IsDevMode && CaptchaToken == utils.Env.Get("CAPTCHA_DEV_PASSCODE", "dev_passcode") {
|
if utils.IsDevMode && CaptchaToken == utils.Env.Get(constant.EnvKeyPasscode, constant.DefaultCaptchaDevPasscode) {
|
||||||
// 开发模式直接通过密钥
|
// 开发模式直接通过密钥,开启开发模式后,Captcha可被绕过,请注意安全
|
||||||
c.Next(ctx)
|
c.Next(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,13 @@ func (s *UserService) UserRegister(req *dto.UserRegisterReq) (*dto.UserRegisterR
|
|||||||
|
|
||||||
func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) {
|
func (s *UserService) RequestVerifyEmail(req *dto.VerifyEmailReq) (*dto.VerifyEmailResp, error) {
|
||||||
verifyCode := utils.RequestEmailVerify(req.Email)
|
verifyCode := utils.RequestEmailVerify(req.Email)
|
||||||
template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{})
|
template, err := static.RenderTemplate("email/verification-code.tmpl", map[string]interface{}{
|
||||||
|
"Title": "NEO-BLOG",
|
||||||
|
"Email": req.Email,
|
||||||
|
"VerifyCode": verifyCode,
|
||||||
|
"Expire": 10,
|
||||||
|
"Details": "你正在验证电子邮件所有权",
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errs.ErrInternalServer
|
return nil, errs.ErrInternalServer
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ const (
|
|||||||
RoleEditor = "editor" // 能够发布和管理自己内容的用户
|
RoleEditor = "editor" // 能够发布和管理自己内容的用户
|
||||||
RoleAdmin = "admin"
|
RoleAdmin = "admin"
|
||||||
DefaultFileBasePath = "./data/uploads"
|
DefaultFileBasePath = "./data/uploads"
|
||||||
EnvKeyBaseUrl = "BASE_URL" // 环境变量:基础URL
|
EnvKeyBaseUrl = "BASE_URL" // 环境变量:基础URL
|
||||||
|
EnvKeyPasscode = "\"CAPTCHA_DEV_PASSCODE\""
|
||||||
EnvKeyCaptchaProvider = "CAPTCHA_PROVIDER" // captcha提供者
|
EnvKeyCaptchaProvider = "CAPTCHA_PROVIDER" // captcha提供者
|
||||||
EnvKeyCaptchaSecreteKey = "CAPTCHA_SECRET_KEY" // captcha站点密钥
|
EnvKeyCaptchaSecreteKey = "CAPTCHA_SECRET_KEY" // captcha站点密钥
|
||||||
EnvKeyCaptchaUrl = "CAPTCHA_URL" // 某些自托管的captcha的url
|
EnvKeyCaptchaUrl = "CAPTCHA_URL" // 某些自托管的captcha的url
|
||||||
@ -64,7 +65,8 @@ const (
|
|||||||
OidcUri = "/user/oidc/login" // OIDC登录URI
|
OidcUri = "/user/oidc/login" // OIDC登录URI
|
||||||
OidcProviderTypeMisskey = "misskey" // OIDC提供者类型:Misskey
|
OidcProviderTypeMisskey = "misskey" // OIDC提供者类型:Misskey
|
||||||
OidcProviderTypeOauth2 = "oauth2" // OIDC提供者类型:GitHub
|
OidcProviderTypeOauth2 = "oauth2" // OIDC提供者类型:GitHub
|
||||||
DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl
|
DefaultCaptchaDevPasscode = "dev_passcode"
|
||||||
|
DefaultBaseUrl = "http://localhost:3000" // 默认BaseUrl
|
||||||
DefaultPasswordSalt = "default_salt_114514"
|
DefaultPasswordSalt = "default_salt_114514"
|
||||||
TargetTypePost = "post"
|
TargetTypePost = "post"
|
||||||
TargetTypeComment = "comment"
|
TargetTypeComment = "comment"
|
||||||
|
@ -25,21 +25,17 @@ type EmailConfig struct {
|
|||||||
SSL bool // 是否使用SSL
|
SSL bool // 是否使用SSL
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendTemplate 发送HTML模板,从配置文件中读取邮箱配置,支持上下文控制
|
func (e *emailUtils) RenderTemplate(htmlTemplate string, data map[string]interface{}) (string, error) {
|
||||||
func (e *emailUtils) SendTemplate(emailConfig *EmailConfig, target, subject, htmlTemplate string, data map[string]interface{}) error {
|
|
||||||
// 使用Go的模板系统处理HTML模板
|
// 使用Go的模板系统处理HTML模板
|
||||||
tmpl, err := template.New("email").Parse(htmlTemplate)
|
tmpl, err := template.New("email").Parse(htmlTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析模板失败: %w", err)
|
return "", fmt.Errorf("解析模板失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.Execute(&buf, data); err != nil {
|
if err := tmpl.Execute(&buf, data); err != nil {
|
||||||
return fmt.Errorf("执行模板失败: %w", err)
|
return "", fmt.Errorf("执行模板失败: %w", err)
|
||||||
}
|
}
|
||||||
|
return buf.String(), nil
|
||||||
// 发送处理后的HTML内容
|
|
||||||
return e.SendEmail(emailConfig, target, subject, buf.String(), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEmail 使用gomail库发送邮件
|
// SendEmail 使用gomail库发送邮件
|
||||||
|
16
pkg/utils/email_test.go
Normal file
16
pkg/utils/email_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEmailUtils_SendEmail(t *testing.T) {
|
||||||
|
templateString := "{{.A}} {{.B}} {{.C}}"
|
||||||
|
data := map[string]interface{}{"A": 1, "B": 2, "C": 3}
|
||||||
|
rendered, err := Email.RenderTemplate(templateString, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RenderTemplate failed: %v", err)
|
||||||
|
}
|
||||||
|
expected := "1 2 3"
|
||||||
|
if rendered != expected {
|
||||||
|
t.Errorf("RenderTemplate = %q; want %q", rendered, expected)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
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 { User } from '@/models/user'
|
import type { User } from '@/models/user'
|
||||||
import { CaptchaProvider } from '@/models/captcha'
|
import { CaptchaProvider } from '@/types/captcha'
|
||||||
import axiosClient from './client'
|
import axiosClient from './client'
|
||||||
|
|
||||||
export async function userLogin(
|
export async function userLogin(
|
||||||
@ -97,8 +97,8 @@ export async function updateUser(data: Partial<User>): Promise<BaseResponse<User
|
|||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestEmailVerifyCode(email: string): Promise<BaseResponse<{ coolDown: number }>> {
|
export async function requestEmailVerifyCode({email, captchaToken}: {email: string, captchaToken?: string}): Promise<BaseResponse<{ coolDown: number }>> {
|
||||||
const res = await axiosClient.post<BaseResponse<{ coolDown: number }>>('/user/email/verify', { email })
|
const res = await axiosClient.post<BaseResponse<{ coolDown: number }>>('/user/email/verify', { email }, { headers: { 'X-Captcha-Token': captchaToken } })
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { RegisterForm } from '@/components/auth/register/register-form'
|
|||||||
function PageContent() {
|
function PageContent() {
|
||||||
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">
|
||||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
<div className="flex w-full max-w-md flex-col gap-6">
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<RegisterForm />
|
<RegisterForm />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { ResetPasswordForm } from "@/components/auth/reset-password/reset-passwo
|
|||||||
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">
|
||||||
<div className="flex w-full max-w-sm flex-col gap-6">
|
<div className="flex w-full max-w-md flex-col gap-6">
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<ResetPasswordForm />
|
<ResetPasswordForm />
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,7 @@ 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 "@/components/common/captcha"
|
import Captcha from "@/components/common/captcha"
|
||||||
import { CaptchaProvider } from "@/models/captcha"
|
import { CaptchaProvider } from "@/types/captcha"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useAuth } from "@/contexts/auth-context"
|
import { useAuth } from "@/contexts/auth-context"
|
||||||
import { registerPath, resetPasswordPath } from "@/hooks/use-route"
|
import { registerPath, resetPasswordPath } from "@/hooks/use-route"
|
||||||
|
@ -15,7 +15,7 @@ import { getCaptchaConfig, requestEmailVerifyCode, userRegister } from "@/api/us
|
|||||||
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 "@/components/common/captcha"
|
import Captcha from "@/components/common/captcha"
|
||||||
import { CaptchaProvider } from "@/models/captcha"
|
import { CaptchaProvider } from "@/types/captcha"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
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"
|
||||||
@ -41,13 +41,22 @@ export function RegisterForm({
|
|||||||
url?: string
|
url?: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
const [captchaToken, setCaptchaToken] = useState<string | null>(null)
|
||||||
const [isLogging, setIsLogging] = useState(false)
|
|
||||||
const [refreshCaptchaKey, setRefreshCaptchaKey] = useState(0)
|
const [refreshCaptchaKey, setRefreshCaptchaKey] = useState(0)
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [verifyCode, setVerifyCode] = useState('')
|
const [verifyCode, setVerifyCode] = useState('')
|
||||||
|
const [sendingVerifyCode, setSendingVerifyCode] = useState(false)
|
||||||
|
const [registering, setRegistering] = useState(false)
|
||||||
|
const [coolDown, setCoolDown] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (coolDown <= 0) return
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setCoolDown(c => (c > 1 ? c - 1 : 0))
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [coolDown])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCaptchaConfig()
|
getCaptchaConfig()
|
||||||
@ -68,13 +77,19 @@ export function RegisterForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSendVerifyCode = () => {
|
const handleSendVerifyCode = () => {
|
||||||
requestEmailVerifyCode(email)
|
if (!email || coolDown > 0 || sendingVerifyCode) return;
|
||||||
|
setSendingVerifyCode(true);
|
||||||
|
requestEmailVerifyCode({ email, captchaToken: captchaToken || '' })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(t("send_verify_code_success"))
|
toast.success(t("send_verify_code_success"))
|
||||||
})
|
})
|
||||||
.catch((error: BaseErrorResponse) => {
|
.catch((error: BaseErrorResponse) => {
|
||||||
toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`)
|
toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setSendingVerifyCode(false);
|
||||||
|
setCoolDown(60);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRegister = () => {
|
const handleRegister = () => {
|
||||||
@ -83,9 +98,9 @@ export function RegisterForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!captchaToken) {
|
if (!captchaToken) {
|
||||||
toast.error(t("please_complete_captcha_verification"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setRegistering(true)
|
||||||
userRegister({ username, password, email, verifyCode, captchaToken })
|
userRegister({ username, password, email, verifyCode, captchaToken })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
toast.success(t("register_success") + ` ${res.data.user.nickname || res.data.user.username}`);
|
toast.success(t("register_success") + ` ${res.data.user.nickname || res.data.user.username}`);
|
||||||
@ -98,12 +113,12 @@ export function RegisterForm({
|
|||||||
setCaptchaToken(null)
|
setCaptchaToken(null)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLogging(false)
|
setRegistering(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
<div className={cn("", className)} {...props}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-xl">{t("title")}</CardTitle>
|
<CardTitle className="text-xl">{t("title")}</CardTitle>
|
||||||
@ -164,11 +179,10 @@ export function RegisterForm({
|
|||||||
<InputOTPControlled
|
<InputOTPControlled
|
||||||
onChange={value => setVerifyCode(value)}
|
onChange={value => setVerifyCode(value)}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSendVerifyCode} disabled={!email} variant="outline" className="border-2" type="button">
|
<Button onClick={handleSendVerifyCode} disabled={!email || coolDown > 0 || sendingVerifyCode} variant="outline" className="border-2" type="button">
|
||||||
{commonT("send_verify_code")}
|
{commonT("send_verify_code")}{coolDown > 0 ? `(${coolDown})` : ""}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{captchaProps &&
|
{captchaProps &&
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
@ -179,9 +193,9 @@ export function RegisterForm({
|
|||||||
type="button"
|
type="button"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={handleRegister}
|
onClick={handleRegister}
|
||||||
disabled={!captchaToken || isLogging || !username || !password || !email}
|
disabled={!captchaToken || registering || !username || !password || !email || !(verifyCode.length == 6)}
|
||||||
>
|
>
|
||||||
{isLogging ? t("registering") : t("register")}
|
{registering ? t("registering") : t("register")}
|
||||||
</Button>
|
</Button>
|
||||||
{/* 注册链接 */}
|
{/* 注册链接 */}
|
||||||
<div className="text-center text-sm">
|
<div className="text-center text-sm">
|
||||||
|
@ -10,7 +10,7 @@ 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 { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { requestEmailVerifyCode, resetPassword } from "@/api/user"
|
import { requestEmailVerifyCode, resetPassword } from "@/api/user"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
@ -28,15 +28,31 @@ export function ResetPasswordForm({
|
|||||||
const [email, setEmail] = useState("")
|
const [email, setEmail] = useState("")
|
||||||
const [verifyCode, setVerifyCode] = useState("")
|
const [verifyCode, setVerifyCode] = useState("")
|
||||||
const [newPassword, setNewPassword] = useState("")
|
const [newPassword, setNewPassword] = useState("")
|
||||||
|
const [coolDown, setCoolDown] = useState(0)
|
||||||
|
const [sendingVerifyCode, setSendingVerifyCode] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (coolDown <= 0) return
|
||||||
|
const id = setInterval(() => {
|
||||||
|
setCoolDown(c => (c > 1 ? c - 1 : 0))
|
||||||
|
}, 1000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [coolDown])
|
||||||
|
|
||||||
const handleSendVerifyCode = () => {
|
const handleSendVerifyCode = () => {
|
||||||
requestEmailVerifyCode(email)
|
if (coolDown > 0 || !email || sendingVerifyCode) return
|
||||||
|
setSendingVerifyCode(true)
|
||||||
|
requestEmailVerifyCode({ email })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(t("send_verify_code_success"))
|
toast.success(t("send_verify_code_success"))
|
||||||
})
|
})
|
||||||
.catch((error: BaseErrorResponse) => {
|
.catch((error: BaseErrorResponse) => {
|
||||||
toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`)
|
toast.error(`${t("send_verify_code_failed")}: ${error.response.data.message}`)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setSendingVerifyCode(false)
|
||||||
|
setCoolDown(60)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResetPassword = () => {
|
const handleResetPassword = () => {
|
||||||
@ -66,18 +82,23 @@ export function ResetPasswordForm({
|
|||||||
<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}
|
|
||||||
variant="outline"
|
|
||||||
className="border-2"
|
|
||||||
type="button"
|
|
||||||
onClick={handleSendVerifyCode}>{t("send_verify_code")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
<Label htmlFor="verify_code">{t("verify_code")}</Label>
|
<Label htmlFor="verify_code">{t("verify_code")}</Label>
|
||||||
<InputOTPControlled onChange={value => setVerifyCode(value)} />
|
<div className="flex gap-3 justify-between">
|
||||||
|
<InputOTPControlled onChange={value => setVerifyCode(value)} />
|
||||||
|
<Button
|
||||||
|
disabled={!email || coolDown > 0}
|
||||||
|
variant="outline"
|
||||||
|
className="border-2"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSendVerifyCode}>
|
||||||
|
{t("send_verify_code")}{coolDown > 0 ? `(${coolDown})` : ""}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
21
web/src/components/common/captcha/dialog-captcha.tsx
Normal file
21
web/src/components/common/captcha/dialog-captcha.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { CaptchaProps } from "@/types/captcha";
|
||||||
|
import AIOCaptchaWidget from ".";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function DialogCaptcha({ className, ...props }: React.ComponentProps<"div"> & CaptchaProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(className)}>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger>{props.children}</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Captcha</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<AIOCaptchaWidget {...props} />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
@ -3,19 +3,12 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { GoogleReCaptcha, GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
import { GoogleReCaptcha, GoogleReCaptchaProvider } from "react-google-recaptcha-v3";
|
||||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||||
import { CaptchaProvider } from "@/models/captcha";
|
|
||||||
import "./captcha.css";
|
import "./captcha.css";
|
||||||
import { TurnstileWidget } from "./turnstile";
|
import { TurnstileWidget } from "./turnstile";
|
||||||
|
import { CaptchaProps, CaptchaProvider } from "@/types/captcha";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type CaptchaProps = {
|
|
||||||
provider: CaptchaProvider;
|
|
||||||
siteKey: string;
|
|
||||||
url?: string;
|
|
||||||
onSuccess: (token: string) => void;
|
|
||||||
onError: (error: string) => void;
|
|
||||||
onAbort?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ReCaptchaWidget(props: CaptchaProps) {
|
export function ReCaptchaWidget(props: CaptchaProps) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CaptchaProps } from ".";
|
|
||||||
import { Turnstile } from "@marsidev/react-turnstile";
|
import { Turnstile } from "@marsidev/react-turnstile";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { CaptchaProps } from "@/types/captcha";
|
||||||
|
|
||||||
const TURNSTILE_TIMEOUT = 15
|
const TURNSTILE_TIMEOUT = 15
|
||||||
// 简单的转圈圈动画
|
// 简单的转圈圈动画
|
||||||
|
@ -34,7 +34,7 @@ export function UserSecurityPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSendVerifyCode = () => {
|
const handleSendVerifyCode = () => {
|
||||||
requestEmailVerifyCode(email)
|
requestEmailVerifyCode({email})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(t("send_verify_code_success"))
|
toast.success(t("send_verify_code_success"))
|
||||||
})
|
})
|
||||||
|
@ -16,6 +16,7 @@ const config = {
|
|||||||
bodyWidthMobile: "100vw",
|
bodyWidthMobile: "100vw",
|
||||||
postsPerPage: 9,
|
postsPerPage: 9,
|
||||||
commentsPerPage: 8,
|
commentsPerPage: 8,
|
||||||
|
verifyCodeCoolDown: 60,
|
||||||
animationDurationSecond: 0.618,
|
animationDurationSecond: 0.618,
|
||||||
footer: {
|
footer: {
|
||||||
text: "Liteyuki ICP备 1145141919810",
|
text: "Liteyuki ICP备 1145141919810",
|
||||||
|
@ -156,6 +156,7 @@
|
|||||||
"Register": {
|
"Register": {
|
||||||
"title": "注册",
|
"title": "注册",
|
||||||
"already_have_account": "已经有账号?",
|
"already_have_account": "已经有账号?",
|
||||||
|
"please_fill_in_all_required_fields": "请填写所有必填项",
|
||||||
"register": "注册",
|
"register": "注册",
|
||||||
"registering": "注册中...",
|
"registering": "注册中...",
|
||||||
"register_a_new_account": "注册一个新账号",
|
"register_a_new_account": "注册一个新账号",
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export enum CaptchaProvider {
|
|
||||||
HCAPTCHA = "hcaptcha",
|
|
||||||
MCAPTCHA = "mcaptcha",
|
|
||||||
RECAPTCHA = "recaptcha",
|
|
||||||
TURNSTILE = "turnstile",
|
|
||||||
DISABLE = "disable",
|
|
||||||
}
|
|
16
web/src/types/captcha.ts
Normal file
16
web/src/types/captcha.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export enum CaptchaProvider {
|
||||||
|
HCAPTCHA = "hcaptcha",
|
||||||
|
MCAPTCHA = "mcaptcha",
|
||||||
|
RECAPTCHA = "recaptcha",
|
||||||
|
TURNSTILE = "turnstile",
|
||||||
|
DISABLE = "disable",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CaptchaProps = {
|
||||||
|
provider: CaptchaProvider;
|
||||||
|
siteKey: string;
|
||||||
|
url?: string;
|
||||||
|
onSuccess: (token: string) => void;
|
||||||
|
onError: (error: string) => void;
|
||||||
|
onAbort?: () => void;
|
||||||
|
};
|
Reference in New Issue
Block a user