fix: 修复帖子相关逻辑的严重安全与错误处理问题
- 修复 Service 层所有 FindByID 错误均返回 ErrUserNotFound 的严重 bug,新增 findPost/findPostWithAuthor 正确区分 gorm.ErrRecordNotFound 和数据库错误 - 新增帖子相关错误哨兵:ErrPostNotFound/ErrPostCannotEdit/ErrPostCannotSubmit/ErrPostCannotApprove/ErrPostCannotReject/ErrPostCannotUnlock - 修复 ShowAPI 缺少权限控制,非 approved 帖子可被任意用户通过 API 获取 - 修复 EditPage 缺少权限检查,非作者可访问编辑页面 - 修复 Update/Delete API 中 uid 断言未做 ok 检查 - Controller 层区分业务错误和内部错误,避免透传 Service 层错误信息给客户端 - 将 postStore 接口从 post_service.go 移至 repository.go,与其他仓储接口保持一致 - 移除 new.html 中孤立的 </template> 标签
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"metazone.cc/metalab/internal/model"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// sanitizePolicy Tiptap 输出 HTML 的消毒策略
|
||||
@ -24,18 +26,6 @@ var sanitizePolicy = func() *bluemonday.Policy {
|
||||
return p
|
||||
}()
|
||||
|
||||
// postStore 帖子服务所需的最小仓储接口(ISP)
|
||||
type postStore interface {
|
||||
Create(post *model.Post) error
|
||||
FindByID(id uint) (*model.Post, error)
|
||||
FindByIDWithAuthor(id uint) (*model.Post, error)
|
||||
FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error)
|
||||
FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error)
|
||||
Update(post *model.Post) error
|
||||
SoftDelete(id uint) error
|
||||
Restore(id uint) error
|
||||
}
|
||||
|
||||
// PostService 帖子业务逻辑
|
||||
type PostService struct {
|
||||
repo postStore
|
||||
@ -55,6 +45,30 @@ func (s *PostService) auditEnabled() bool {
|
||||
return s.ss.IsAuditEnabled()
|
||||
}
|
||||
|
||||
// findPost 按 ID 查找帖子,区分"未找到"和"数据库错误"
|
||||
func (s *PostService) findPost(id uint) (*model.Post, error) {
|
||||
post, err := s.repo.FindByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrPostNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("查询帖子失败: %w", err)
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// findPostWithAuthor 按 ID 查找帖子(含作者名),区分"未找到"和"数据库错误"
|
||||
func (s *PostService) findPostWithAuthor(id uint) (*model.Post, error) {
|
||||
post, err := s.repo.FindByIDWithAuthor(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, common.ErrPostNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("查询帖子失败: %w", err)
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
// sanitizeHTML 对 Tiptap 输出的 HTML 进行消毒
|
||||
// 前端 WYSIWYG 编辑器直接输出 HTML,后端仅做安全消毒
|
||||
func (s *PostService) sanitizeHTML(body string) string {
|
||||
@ -86,11 +100,7 @@ func (s *PostService) Create(userID uint, title, body string) (*model.Post, erro
|
||||
|
||||
// GetByID 按 ID 获取帖子(含作者名,仅 approved 公开可见)
|
||||
func (s *PostService) GetByID(id uint) (*model.Post, error) {
|
||||
post, err := s.repo.FindByIDWithAuthor(id)
|
||||
if err != nil {
|
||||
return nil, common.ErrUserNotFound // 复用哨兵表示资源不存在
|
||||
}
|
||||
return post, nil
|
||||
return s.findPostWithAuthor(id)
|
||||
}
|
||||
|
||||
// List 公开帖子列表(仅 approved)
|
||||
@ -109,13 +119,13 @@ func (s *PostService) ListAdmin(keyword, status string, page, pageSize int) ([]m
|
||||
|
||||
// Update 编辑帖子(权限在 controller 层检查)
|
||||
func (s *PostService) Update(postID uint, title, body string) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
|
||||
if post.Status == model.PostStatusPending || post.Status == model.PostStatusLocked {
|
||||
return fmt.Errorf("当前状态不允许编辑")
|
||||
return common.ErrPostCannotEdit
|
||||
}
|
||||
|
||||
post.Title = title
|
||||
@ -138,12 +148,12 @@ func (s *PostService) Delete(postID uint) error {
|
||||
|
||||
// SubmitForAudit 提交审核:draft → pending
|
||||
func (s *PostService) SubmitForAudit(postID uint) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
if post.Status != model.PostStatusDraft && post.Status != model.PostStatusRejected {
|
||||
return fmt.Errorf("仅草稿和已退回帖子可提交审核")
|
||||
return common.ErrPostCannotSubmit
|
||||
}
|
||||
post.Status = model.PostStatusPending
|
||||
return s.repo.Update(post)
|
||||
@ -151,12 +161,12 @@ func (s *PostService) SubmitForAudit(postID uint) error {
|
||||
|
||||
// Approve 审核通过:pending → approved
|
||||
func (s *PostService) Approve(postID uint) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
if post.Status != model.PostStatusPending {
|
||||
return fmt.Errorf("仅待审核帖子可通过")
|
||||
return common.ErrPostCannotApprove
|
||||
}
|
||||
post.Status = model.PostStatusApproved
|
||||
post.RejectReason = ""
|
||||
@ -165,23 +175,23 @@ func (s *PostService) Approve(postID uint) error {
|
||||
|
||||
// Reject 退回:pending/approved → rejected
|
||||
func (s *PostService) Reject(postID uint, reason string) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
if post.Status != model.PostStatusPending && post.Status != model.PostStatusApproved {
|
||||
return fmt.Errorf("仅待审核和已发布帖子可退回")
|
||||
return common.ErrPostCannotReject
|
||||
}
|
||||
post.Status = model.PostStatusRejected
|
||||
post.RejectReason = reason
|
||||
return s.repo.Update(post)
|
||||
}
|
||||
|
||||
// Lock 锁定:任意状态 → locked
|
||||
// Lock 锁定:任意状态 → locked(管理员可随时锁定)
|
||||
func (s *PostService) Lock(postID uint) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
post.Status = model.PostStatusLocked
|
||||
return s.repo.Update(post)
|
||||
@ -189,12 +199,12 @@ func (s *PostService) Lock(postID uint) error {
|
||||
|
||||
// Unlock 解锁:locked → 草稿
|
||||
func (s *PostService) Unlock(postID uint) error {
|
||||
post, err := s.repo.FindByID(postID)
|
||||
post, err := s.findPost(postID)
|
||||
if err != nil {
|
||||
return common.ErrUserNotFound
|
||||
return err
|
||||
}
|
||||
if post.Status != model.PostStatusLocked {
|
||||
return fmt.Errorf("仅锁定状态可解锁")
|
||||
return common.ErrPostCannotUnlock
|
||||
}
|
||||
post.Status = model.PostStatusDraft
|
||||
return s.repo.Update(post)
|
||||
|
||||
Reference in New Issue
Block a user