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:
2026-05-28 14:44:22 +08:00
parent 36b18c0ffe
commit e45d66fb77
6 changed files with 141 additions and 47 deletions

View File

@ -1,6 +1,7 @@
package admin
import (
"errors"
"net/http"
"strconv"
@ -72,7 +73,11 @@ func (ctrl *AdminPostController) Approve(c *gin.Context) {
}
if err := ctrl.postService.Approve(uint(id)); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostCannotApprove) || errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "审核失败")
}
return
}
@ -94,7 +99,11 @@ func (ctrl *AdminPostController) Reject(c *gin.Context) {
}
if err := ctrl.postService.Reject(uint(id), req.Reason); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostCannotReject) || errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "退回失败")
}
return
}
@ -110,7 +119,11 @@ func (ctrl *AdminPostController) Lock(c *gin.Context) {
}
if err := ctrl.postService.Lock(uint(id)); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "锁定失败")
}
return
}
@ -126,7 +139,11 @@ func (ctrl *AdminPostController) Unlock(c *gin.Context) {
}
if err := ctrl.postService.Unlock(uint(id)); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostCannotUnlock) || errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "解锁失败")
}
return
}
@ -142,7 +159,7 @@ func (ctrl *AdminPostController) Restore(c *gin.Context) {
}
if err := ctrl.postService.Restore(uint(id)); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
common.Error(c, http.StatusInternalServerError, "恢复失败")
return
}

View File

@ -1,6 +1,7 @@
package controller
import (
"errors"
"fmt"
"html/template"
"mime/multipart"
@ -144,6 +145,22 @@ func (ctrl *PostController) EditPage(c *gin.Context) {
return
}
// 权限检查:仅作者和 moderator+ 可编辑
var uid uint
var role string
if uidObj, exists := c.Get("uid"); exists {
uid, _ = uidObj.(uint)
}
if roleObj, exists := c.Get("role"); exists {
role, _ = roleObj.(string)
}
if post.UserID != uid && !model.HasMinRole(role, model.RoleModerator) {
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
"Title": "未找到",
}))
return
}
c.HTML(http.StatusOK, "posts/new.html", common.BuildPageData(c, gin.H{
"Title": "编辑帖子",
"Post": post,
@ -178,7 +195,11 @@ func (ctrl *PostController) Create(c *gin.Context) {
// Update API 编辑帖子
func (ctrl *PostController) Update(c *gin.Context) {
uidObj, _ := c.Get("uid")
uid, _ := uidObj.(uint)
uid, ok := uidObj.(uint)
if !ok {
common.Error(c, http.StatusUnauthorized, "请先登录")
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@ -206,7 +227,11 @@ func (ctrl *PostController) Update(c *gin.Context) {
}
if err := ctrl.postService.Update(uint(id), req.Title, req.Body); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostCannotEdit) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "编辑失败")
}
return
}
@ -216,7 +241,11 @@ func (ctrl *PostController) Update(c *gin.Context) {
// Delete API 删除帖子
func (ctrl *PostController) Delete(c *gin.Context) {
uidObj, _ := c.Get("uid")
uid, _ := uidObj.(uint)
uid, ok := uidObj.(uint)
if !ok {
common.Error(c, http.StatusUnauthorized, "请先登录")
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
@ -254,7 +283,11 @@ func (ctrl *PostController) Submit(c *gin.Context) {
}
if err := ctrl.postService.SubmitForAudit(uint(id)); err != nil {
common.Error(c, http.StatusBadRequest, err.Error())
if errors.Is(err, common.ErrPostCannotSubmit) || errors.Is(err, common.ErrPostNotFound) {
common.Error(c, http.StatusBadRequest, err.Error())
} else {
common.Error(c, http.StatusInternalServerError, "提交审核失败")
}
return
}
@ -303,6 +336,24 @@ func (ctrl *PostController) ShowAPI(c *gin.Context) {
return
}
// 权限控制:非 approved 帖子仅作者和 moderator+ 可见
if post.Status != model.PostStatusApproved {
var uid uint
var role string
if uidObj, exists := c.Get("uid"); exists {
uid, _ = uidObj.(uint)
}
if roleObj, exists := c.Get("role"); exists {
role, _ = roleObj.(string)
}
isAuthor := uid == post.UserID
isModerator := model.HasMinRole(role, model.RoleModerator)
if !isAuthor && !isModerator {
common.Error(c, http.StatusNotFound, "帖子不存在")
return
}
}
common.Ok(c, post)
}