diff --git a/internal/dto/comment.go b/internal/dto/comment.go index a7ccc79..377dcd4 100644 --- a/internal/dto/comment.go +++ b/internal/dto/comment.go @@ -13,6 +13,7 @@ type CommentDto struct { ReplyCount int64 `json:"reply_count"` // 回复数量 LikeCount uint64 `json:"like_count"` // 点赞数量 IsLiked bool `json:"is_liked"` // 当前用户是否点赞 + IsPrivate bool `json:"is_private"` } type CreateCommentReq struct { diff --git a/internal/model/oidc_config.go b/internal/model/oidc_config.go index 4fd5daa..7edfba7 100644 --- a/internal/model/oidc_config.go +++ b/internal/model/oidc_config.go @@ -1,116 +1,117 @@ package model import ( - "fmt" - "github.com/sirupsen/logrus" - "github.com/snowykami/neo-blog/internal/dto" - "gorm.io/gorm" - "resty.dev/v3" - "time" + "fmt" + "time" + + "github.com/sirupsen/logrus" + "github.com/snowykami/neo-blog/internal/dto" + "gorm.io/gorm" + "resty.dev/v3" ) type OidcConfig struct { - gorm.Model - Name string `gorm:"uniqueIndex"` // OIDC配置名称,唯一 - ClientID string // 客户端ID - ClientSecret string // 客户端密钥 - DisplayName string // 显示名称,例如:轻雪通行证 - Icon string // 图标url,为空则使用内置默认图标 - OidcDiscoveryUrl string // OpenID自动发现URL,例如 :https://pass.liteyuki.icu/.well-known/openid-configuration - Enabled bool `gorm:"default:true"` // 是否启用 - Type string `gorm:"oauth2"` // OIDC类型,默认为oauth2,也可以为misskey - // 以下字段为自动获取字段,每次更新配置时自动填充 - Issuer string - AuthorizationEndpoint string - TokenEndpoint string - UserInfoEndpoint string - JwksUri string + gorm.Model + Name string `gorm:"uniqueIndex"` // OIDC配置名称,唯一 + ClientID string // 客户端ID + ClientSecret string // 客户端密钥 + DisplayName string // 显示名称,例如:轻雪通行证 + Icon string // 图标url,为空则使用内置默认图标 + OidcDiscoveryUrl string // OpenID自动发现URL,例如 :https://pass.liteyuki.org/.well-known/openid-configuration + Enabled bool `gorm:"default:true"` // 是否启用 + Type string `gorm:"oauth2"` // OIDC类型,默认为oauth2,也可以为misskey + // 以下字段为自动获取字段,每次更新配置时自动填充 + Issuer string + AuthorizationEndpoint string + TokenEndpoint string + UserInfoEndpoint string + JwksUri string } type oidcDiscoveryResp struct { - Issuer string `json:"issuer" validate:"required"` - AuthorizationEndpoint string `json:"authorization_endpoint" validate:"required"` - TokenEndpoint string `json:"token_endpoint" validate:"required"` - UserInfoEndpoint string `json:"userinfo_endpoint" validate:"required"` - JwksUri string `json:"jwks_uri" validate:"required"` - // 可选字段 - RegistrationEndpoint string `json:"registration_endpoint,omitempty"` - ScopesSupported []string `json:"scopes_supported,omitempty"` - ResponseTypesSupported []string `json:"response_types_supported,omitempty"` - GrantTypesSupported []string `json:"grant_types_supported,omitempty"` - SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` - IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` - ClaimsSupported []string `json:"claims_supported,omitempty"` - EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` + Issuer string `json:"issuer" validate:"required"` + AuthorizationEndpoint string `json:"authorization_endpoint" validate:"required"` + TokenEndpoint string `json:"token_endpoint" validate:"required"` + UserInfoEndpoint string `json:"userinfo_endpoint" validate:"required"` + JwksUri string `json:"jwks_uri" validate:"required"` + // 可选字段 + RegistrationEndpoint string `json:"registration_endpoint,omitempty"` + ScopesSupported []string `json:"scopes_supported,omitempty"` + ResponseTypesSupported []string `json:"response_types_supported,omitempty"` + GrantTypesSupported []string `json:"grant_types_supported,omitempty"` + SubjectTypesSupported []string `json:"subject_types_supported,omitempty"` + IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"` + ClaimsSupported []string `json:"claims_supported,omitempty"` + EndSessionEndpoint string `json:"end_session_endpoint,omitempty"` } func updateOidcConfigFromUrl(url string, typ string) (*oidcDiscoveryResp, error) { - client := resty.New() - client.SetTimeout(10 * time.Second) // 设置超时时间 - var discovery oidcDiscoveryResp - resp, err := client.R(). - SetHeader("Accept", "application/json"). - SetResult(&discovery). - Get(url) - if err != nil { - return nil, fmt.Errorf("请求OIDC发现端点失败: %w", err) - } - if resp.StatusCode() != 200 { - return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode()) - } - // 验证必要字段 - if typ == "misskey" { - discovery.UserInfoEndpoint = discovery.Issuer + "/api/users/me" // Misskey的用户信息端点 - discovery.JwksUri = discovery.Issuer + "/api/jwks" - } - fmt.Println(discovery) - if discovery.Issuer == "" || - discovery.AuthorizationEndpoint == "" || - discovery.TokenEndpoint == "" || - discovery.UserInfoEndpoint == "" || - discovery.JwksUri == "" { - return nil, fmt.Errorf("OIDC发现端点响应缺少必要字段") - } - return &discovery, nil + client := resty.New() + client.SetTimeout(10 * time.Second) // 设置超时时间 + var discovery oidcDiscoveryResp + resp, err := client.R(). + SetHeader("Accept", "application/json"). + SetResult(&discovery). + Get(url) + if err != nil { + return nil, fmt.Errorf("请求OIDC发现端点失败: %w", err) + } + if resp.StatusCode() != 200 { + return nil, fmt.Errorf("请求OIDC发现端点失败,状态码: %d", resp.StatusCode()) + } + // 验证必要字段 + if typ == "misskey" { + discovery.UserInfoEndpoint = discovery.Issuer + "/api/users/me" // Misskey的用户信息端点 + discovery.JwksUri = discovery.Issuer + "/api/jwks" + } + fmt.Println(discovery) + if discovery.Issuer == "" || + discovery.AuthorizationEndpoint == "" || + discovery.TokenEndpoint == "" || + discovery.UserInfoEndpoint == "" || + discovery.JwksUri == "" { + return nil, fmt.Errorf("OIDC发现端点响应缺少必要字段") + } + return &discovery, nil } func (o *OidcConfig) BeforeSave(tx *gorm.DB) (err error) { - // 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息 - if tx.Statement.Changed("OidcDiscoveryUrl") || o.ID == 0 { - logrus.Infof("Updating OIDC config for %s, OidcDiscoveryUrl: %s", o.Name, o.OidcDiscoveryUrl) - discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl, o.Type) - if err != nil { - logrus.Error("Updating OIDC config failed: ", err) - return fmt.Errorf("updating OIDC config failed: %w", err) - } - o.Issuer = discoveryResp.Issuer - o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint - o.TokenEndpoint = discoveryResp.TokenEndpoint - o.UserInfoEndpoint = discoveryResp.UserInfoEndpoint - o.JwksUri = discoveryResp.JwksUri - } - return nil + // 只有在创建新记录或更新 OidcDiscoveryUrl 字段时才更新端点信息 + if tx.Statement.Changed("OidcDiscoveryUrl") || o.ID == 0 { + logrus.Infof("Updating OIDC config for %s, OidcDiscoveryUrl: %s", o.Name, o.OidcDiscoveryUrl) + discoveryResp, err := updateOidcConfigFromUrl(o.OidcDiscoveryUrl, o.Type) + if err != nil { + logrus.Error("Updating OIDC config failed: ", err) + return fmt.Errorf("updating OIDC config failed: %w", err) + } + o.Issuer = discoveryResp.Issuer + o.AuthorizationEndpoint = discoveryResp.AuthorizationEndpoint + o.TokenEndpoint = discoveryResp.TokenEndpoint + o.UserInfoEndpoint = discoveryResp.UserInfoEndpoint + o.JwksUri = discoveryResp.JwksUri + } + return nil } // ToUserDto 返回给用户侧 func (o *OidcConfig) ToUserDto() *dto.UserOidcConfigDto { - return &dto.UserOidcConfigDto{ - Name: o.Name, - DisplayName: o.DisplayName, - Icon: o.Icon, - } + return &dto.UserOidcConfigDto{ + Name: o.Name, + DisplayName: o.DisplayName, + Icon: o.Icon, + } } // ToAdminDto 返回给管理员侧 func (o *OidcConfig) ToAdminDto() *dto.AdminOidcConfigDto { - return &dto.AdminOidcConfigDto{ - ID: o.ID, - Name: o.Name, - ClientID: o.ClientID, - ClientSecret: o.ClientSecret, - DisplayName: o.DisplayName, - Icon: o.Icon, - OidcDiscoveryUrl: o.OidcDiscoveryUrl, - Enabled: o.Enabled, - } + return &dto.AdminOidcConfigDto{ + ID: o.ID, + Name: o.Name, + ClientID: o.ClientID, + ClientSecret: o.ClientSecret, + DisplayName: o.DisplayName, + Icon: o.Icon, + OidcDiscoveryUrl: o.OidcDiscoveryUrl, + Enabled: o.Enabled, + } } diff --git a/internal/repo/comment.go b/internal/repo/comment.go index c6dfc82..89593e8 100644 --- a/internal/repo/comment.go +++ b/internal/repo/comment.go @@ -110,7 +110,7 @@ func (cr *CommentRepo) UpdateComment(comment *model.Comment) error { return errs.New(http.StatusBadRequest, "invalid comment ID", nil) } - if err := GetDB().Updates(comment).Error; err != nil { + if err := GetDB().Select("IsPrivate", "Content").Updates(comment).Error; err != nil { return err } @@ -204,6 +204,7 @@ func (cr *CommentRepo) ListComments(currentUserID, targetID, commentID uint, tar } else { query = query.Where("target_id = ? AND target_type = ?", targetID, targetType) } + items, _, err := PaginateQuery[model.Comment](query, page, size, orderBy, desc) if err != nil { diff --git a/internal/repo/init.go b/internal/repo/init.go index 5e7a569..4385b02 100644 --- a/internal/repo/init.go +++ b/internal/repo/init.go @@ -3,6 +3,9 @@ package repo import ( "errors" "fmt" + "os" + "path/filepath" + "github.com/glebarez/sqlite" "github.com/sirupsen/logrus" "github.com/snowykami/neo-blog/internal/model" @@ -10,8 +13,6 @@ import ( "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" - "os" - "path/filepath" ) var db *gorm.DB diff --git a/internal/repo/oidc_config.go b/internal/repo/oidc_config.go index aa0be4a..e1afd62 100644 --- a/internal/repo/oidc_config.go +++ b/internal/repo/oidc_config.go @@ -1,9 +1,10 @@ package repo import ( + "net/http" + "github.com/snowykami/neo-blog/internal/model" "github.com/snowykami/neo-blog/pkg/errs" - "net/http" ) type oidcRepo struct { @@ -62,7 +63,9 @@ func (o *oidcRepo) UpdateOidcConfig(oidcConfig *model.OidcConfig) error { if oidcConfig.ID == 0 { return errs.New(http.StatusBadRequest, "invalid OIDC config ID", nil) } - if err := GetDB().Select("Enabled").Updates(oidcConfig).Error; err != nil { + if err := GetDB().Select("Name", "ClientID", "ClientSecret", + "DisplayName", "Icon", "OidcDiscoveryUrl", + "Enabled", "Type").Updates(oidcConfig).Error; err != nil { return err } return nil diff --git a/internal/service/comment.go b/internal/service/comment.go index 5bfb864..85218d1 100644 --- a/internal/service/comment.go +++ b/internal/service/comment.go @@ -157,6 +157,7 @@ func (cs *CommentService) GetCommentList(ctx context.Context, req *dto.GetCommen ReplyCount: replyCount, LikeCount: comment.LikeCount, IsLiked: isLiked, + IsPrivate: comment.IsPrivate, } commentDtos = append(commentDtos, commentDto) } diff --git a/pkg/utils/captcha.go b/pkg/utils/captcha.go index 8ef29c4..040c167 100644 --- a/pkg/utils/captcha.go +++ b/pkg/utils/captcha.go @@ -1,10 +1,10 @@ package utils import ( - "fmt" + "fmt" - "github.com/snowykami/neo-blog/pkg/constant" - "resty.dev/v3" + "github.com/snowykami/neo-blog/pkg/constant" + "resty.dev/v3" ) type captchaUtils struct{} @@ -12,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/src/components/blog-post/blog-post.tsx b/web/src/components/blog-post/blog-post.tsx index f4e40ff..d5e8b46 100644 --- a/web/src/components/blog-post/blog-post.tsx +++ b/web/src/components/blog-post/blog-post.tsx @@ -4,7 +4,7 @@ import { Calendar, Clock, FileText, Flame, Heart, MessageCircle, PenLine, Square import { RenderMarkdown } from "@/components/common/markdown"; import { isMobileByUA } from "@/utils/server/device"; import { calculateReadingTime } from "@/utils/common/post"; -import {CommentSection} from "@/components/neo-comment"; +import {CommentSection} from "@/components/comment"; import { TargetType } from '../../models/types'; function PostMeta({ post }: { post: Post }) { diff --git a/web/src/components/neo-comment/comment-input.tsx b/web/src/components/comment/comment-input.tsx similarity index 97% rename from web/src/components/neo-comment/comment-input.tsx rename to web/src/components/comment/comment-input.tsx index 0585a06..15ad715 100644 --- a/web/src/components/neo-comment/comment-input.tsx +++ b/web/src/components/comment/comment-input.tsx @@ -10,6 +10,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox" import { Label } from "@/components/ui/label"; + export function CommentInput( { user, @@ -54,7 +55,7 @@ export function CommentInput(