diff --git a/pkg/utils/captcha.go b/pkg/utils/captcha.go index ce1f541..8ef29c4 100644 --- a/pkg/utils/captcha.go +++ b/pkg/utils/captcha.go @@ -1,9 +1,10 @@ package utils import ( - "fmt" - "github.com/snowykami/neo-blog/pkg/constant" - "resty.dev/v3" + "fmt" + + "github.com/snowykami/neo-blog/pkg/constant" + "resty.dev/v3" ) type captchaUtils struct{} @@ -11,83 +12,83 @@ type captchaUtils struct{} var Captcha = captchaUtils{} type CaptchaConfig struct { - Type string - SiteSecret string // Site secret key for the captcha service - SecretKey string // Secret key for the captcha service + Type string + SiteSecret string // Site secret key for the captcha service + SecretKey string // Secret key for the captcha service } func (c *captchaUtils) GetCaptchaConfigFromEnv() *CaptchaConfig { - return &CaptchaConfig{ - Type: Env.Get("CAPTCHA_TYPE", "disable"), - SiteSecret: Env.Get("CAPTCHA_SITE_SECRET", ""), - SecretKey: Env.Get("CAPTCHA_SECRET_KEY", ""), - } + return &CaptchaConfig{ + Type: Env.Get("CAPTCHA_TYPE", "disable"), + SiteSecret: Env.Get("CAPTCHA_SITE_SECRET", ""), + SecretKey: Env.Get("CAPTCHA_SECRET_KEY", ""), + } } // VerifyCaptcha 根据提供的配置和令牌验证验证码 func (c *captchaUtils) VerifyCaptcha(captchaConfig *CaptchaConfig, captchaToken string) (bool, error) { - restyClient := resty.New() - switch captchaConfig.Type { - case constant.CaptchaTypeDisable: - return true, nil - case constant.CaptchaTypeHCaptcha: - result := make(map[string]any) - resp, err := restyClient.R(). - SetFormData(map[string]string{ - "secret": captchaConfig.SecretKey, - "response": captchaToken, - }).SetResult(&result).Post("https://hcaptcha.com/siteverify") - if err != nil { - return false, err - } - if resp.IsError() { - return false, nil - } - fmt.Printf("%#v\n", result) - if success, ok := result["success"].(bool); ok && success { - return true, nil - } else { - return false, nil - } - case constant.CaptchaTypeTurnstile: - result := make(map[string]any) - resp, err := restyClient.R(). - SetFormData(map[string]string{ - "secret": captchaConfig.SecretKey, - "response": captchaToken, - }).SetResult(&result).Post("https://challenges.cloudflare.com/turnstile/v0/siteverify") - if err != nil { - return false, err - } - if resp.IsError() { - return false, nil - } - fmt.Printf("%#v\n", result) - if success, ok := result["success"].(bool); ok && success { - return true, nil - } else { - return false, nil - } - case constant.CaptchaTypeReCaptcha: - result := make(map[string]any) - resp, err := restyClient.R(). - SetFormData(map[string]string{ - "secret": captchaConfig.SecretKey, - "response": captchaToken, - }).SetResult(&result).Post("https://www.google.com/recaptcha/api/siteverify") - if err != nil { - return false, err - } - if resp.IsError() { - return false, nil - } - fmt.Printf("%#v\n", result) - if success, ok := result["success"].(bool); ok && success { - return true, nil - } else { - return false, nil - } - default: - return false, fmt.Errorf("invalid captcha type: %s", captchaConfig.Type) - } + restyClient := resty.New() + switch captchaConfig.Type { + case constant.CaptchaTypeDisable: + return true, nil + case constant.CaptchaTypeHCaptcha: + result := make(map[string]any) + resp, err := restyClient.R(). + SetFormData(map[string]string{ + "secret": captchaConfig.SecretKey, + "response": captchaToken, + }).SetResult(&result).Post("https://hcaptcha.com/siteverify") + if err != nil { + return false, err + } + if resp.IsError() { + return false, nil + } + fmt.Printf("%#v\n", result) + if success, ok := result["success"].(bool); ok && success { + return true, nil + } else { + return false, nil + } + case constant.CaptchaTypeTurnstile: + result := make(map[string]any) + resp, err := restyClient.R(). + SetFormData(map[string]string{ + "secret": captchaConfig.SecretKey, + "response": captchaToken, + }).SetResult(&result).Post("https://challenges.cloudflare.com/turnstile/v0/siteverify") + if err != nil { + return false, err + } + if resp.IsError() { + return false, nil + } + fmt.Printf("%#v\n", result) + if success, ok := result["success"].(bool); ok && success { + return true, nil + } else { + return false, nil + } + case constant.CaptchaTypeReCaptcha: + result := make(map[string]any) + resp, err := restyClient.R(). + SetFormData(map[string]string{ + "secret": captchaConfig.SecretKey, + "response": captchaToken, + }).SetResult(&result).Post("https://www.google.com/recaptcha/api/siteverify") + if err != nil { + return false, err + } + if resp.IsError() { + return false, nil + } + fmt.Printf("%#v\n", result) + if success, ok := result["success"].(bool); ok && success { + return true, nil + } else { + return false, nil + } + default: + return false, fmt.Errorf("invalid captcha type: %s", captchaConfig.Type) + } } diff --git a/web/package.json b/web/package.json index 4c0fe1b..55cd934 100644 --- a/web/package.json +++ b/web/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-navigation-menu": "^1.2.13", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index df5a36a..0a9c996 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-dialog': specifier: ^1.1.14 version: 1.1.14(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -436,6 +439,22 @@ packages: '@radix-ui/primitive@1.1.2': resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -585,6 +604,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: @@ -3052,6 +3084,24 @@ snapshots: '@radix-ui/primitive@1.1.2': {} + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) @@ -3192,6 +3242,16 @@ snapshots: '@types/react': 19.1.8 '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.8)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.8)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.8 + '@types/react-dom': 19.1.6(@types/react@19.1.8) + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@radix-ui/react-slot': 1.2.3(@types/react@19.1.8)(react@19.1.0) diff --git a/web/src/api/comment.ts b/web/src/api/comment.ts index a7a1b76..a3d1610 100644 --- a/web/src/api/comment.ts +++ b/web/src/api/comment.ts @@ -1,5 +1,5 @@ import axiosClient from './client' -import { UpdateCommentRequest, Comment } from '@/models/comment' +import { Comment } from '@/models/comment' import { PaginationParams } from '@/models/common' import { OrderBy } from '@/models/common' import type { BaseResponse } from '@/models/resp' @@ -32,13 +32,23 @@ export async function createComment( } export async function updateComment( - data: UpdateCommentRequest, + { + id, content, + isPrivate = false + }: { + id: number + content: string + isPrivate?: boolean // 可选字段,默认为 false + } ): Promise> { - const res = await axiosClient.put>(`/comment/c/${data.id}`, data) + const res = await axiosClient.put>(`/comment/c/${id}`, { + content, + isPrivate + }) return res.data } -export async function deleteComment(id: number): Promise { +export async function deleteComment({ id }: { id: number }): Promise { await axiosClient.delete(`/comment/c/${id}`) } diff --git a/web/src/components/comment/comment-animations.css b/web/src/components/comment/comment-animations.css deleted file mode 100644 index 6a84174..0000000 --- a/web/src/components/comment/comment-animations.css +++ /dev/null @@ -1,47 +0,0 @@ -/* 评论区原生动画:淡入、上移 */ -.fade-in { - opacity: 0; - animation: fadeIn 0.5s ease forwards; -} - -.fade-in-up { - opacity: 0; - transform: translateY(16px); - animation: fadeInUp 0.5s cubic-bezier(.33,1,.68,1) forwards; -} - -@keyframes fadeIn { - to { - opacity: 1; - } -} - -@keyframes fadeInUp { - to { - opacity: 1; - transform: translateY(0); - } -}/* 评论区原生动画:淡入、上移 */ -.fade-in { - opacity: 0; - animation: fadeIn 0.5s ease forwards; -} - -.fade-in-up { - opacity: 0; - transform: translateY(16px); - animation: fadeInUp 0.5s cubic-bezier(.33,1,.68,1) forwards; -} - -@keyframes fadeIn { - to { - opacity: 1; - } -} - -@keyframes fadeInUp { - to { - opacity: 1; - transform: translateY(0); - } -} \ No newline at end of file diff --git a/web/src/components/comment/comment-input.tsx b/web/src/components/comment/comment-input.tsx deleted file mode 100644 index 3983abf..0000000 --- a/web/src/components/comment/comment-input.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; -import { Textarea } from "@/components/ui/textarea" -import { getGravatarByUser } from "@/components/common/gravatar" -import { toast } from "sonner"; -import { useState, useEffect } from "react"; -import type { User } from "@/models/user"; -import { getLoginUser } from "@/api/user"; -import { createComment } from "@/api/comment"; - -import { CircleUser } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { TargetType } from "@/models/types"; -import { useToLogin } from "@/hooks/use-route"; -import NeedLogin from "../common/need-login"; - - -import "./comment-animations.css"; - -export function CommentInput( - { targetId, targetType, replyId, onCommentSubmitted }: { targetId: number, targetType: TargetType, replyId: number | null, onCommentSubmitted: () => void } -) { - - const t = useTranslations('Comment') - const toLogin = useToLogin() - const [user, setUser] = useState(null); - const [commentContent, setCommentContent] = useState(""); - - useEffect(() => { - getLoginUser() - .then(response => { - setUser(response.data); - }) - }, []); - - const handleCommentSubmit = async () => { - if (!user) { - toast.error({t("login_required")}); - return; - } - if (!commentContent.trim()) { - toast.error(t("content_required")); - return; - } - await createComment({ - targetType: targetType, - targetId: targetId, - content: commentContent, - replyId: replyId, - isPrivate: false, - }).then(response => { - setCommentContent(""); - toast.success(t("comment_success")); - onCommentSubmitted(); - }).catch(error => { - toast.error(t("comment_failed") + ": " + - error?.response?.data?.message || error?.message - ); - }); - }; - - return ( -
-
- {/* Avatar */} -
- {user && getGravatarByUser(user)} - {!user && } -
- {/* Input Area */} -
-