feat(verification): enforce user verification requirement for posts, comments, and WebSocket connections
Some checks failed
Build Backend / build-docker (push) Has been cancelled
Build Backend / build (push) Has been cancelled

Add verification status checks to protected routes including post creation, updates, deletion, likes, favorites, voting, and WebSocket connections. Also rename RequireVerification middleware to RequireVerified and update error response format with string error codes.
This commit is contained in:
lafay
2026-04-08 02:28:10 +08:00
parent 98b8abd26a
commit ef9a30ee54
7 changed files with 68 additions and 40 deletions

View File

@@ -5,6 +5,7 @@ package main
import (
"carrot_bbs/internal/handler"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/router"
"carrot_bbs/internal/service"
appwire "carrot_bbs/internal/wire"
@@ -24,6 +25,7 @@ func InitializeApp() (*App, error) {
// ProvideRouter 提供路由
func ProvideRouter(
userRepo repository.UserRepository,
userHandler *handler.UserHandler,
postHandler *handler.PostHandler,
commentHandler *handler.CommentHandler,
@@ -58,6 +60,7 @@ func ProvideRouter(
wsHandler *handler.WSHandler,
) *router.Router {
return router.New(
userRepo,
userHandler,
postHandler,
commentHandler,

View File

@@ -132,8 +132,8 @@ func InitializeApp() (*App, error) {
verificationHandler := handler.NewVerificationHandler(verificationService)
adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository)
adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository)
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService)
router := ProvideRouter(userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, logService, userActivityService, casbinService, wsHandler)
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService, userRepository)
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, logService, userActivityService, casbinService, wsHandler)
hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, cache)
app := NewApp(config, db, router, pushService, hotRankWorker, server, logger)
return app, nil
@@ -143,6 +143,7 @@ func InitializeApp() (*App, error) {
// ProvideRouter 提供路由
func ProvideRouter(
userRepo repository.UserRepository,
userHandler *handler.UserHandler,
postHandler *handler.PostHandler,
commentHandler *handler.CommentHandler,
@@ -177,6 +178,7 @@ func ProvideRouter(
wsHandler *handler.WSHandler,
) *router.Router {
return router.New(
userRepo,
userHandler,
postHandler,
commentHandler,

View File

@@ -20,6 +20,7 @@ import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/pkg/ws"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
)
@@ -71,6 +72,7 @@ type WSHandler struct {
groupService service.GroupService
jwtService *service.JWTService
callService service.CallService
userRepo repository.UserRepository
clientSeq uint64
}
@@ -81,6 +83,7 @@ func NewWSHandler(
groupService service.GroupService,
jwtService *service.JWTService,
callService service.CallService,
userRepo repository.UserRepository,
) *WSHandler {
return &WSHandler{
wsHub: wsHub,
@@ -88,6 +91,7 @@ func NewWSHandler(
groupService: groupService,
jwtService: jwtService,
callService: callService,
userRepo: userRepo,
}
}
@@ -118,6 +122,18 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
}
userID := claims.UserID
// 检查用户认证状态
user, err := h.userRepo.GetByID(userID)
if err != nil {
response.InternalServerError(c, "获取用户信息失败")
return
}
if user.VerificationStatus != model.VerificationStatusApproved {
response.ErrorWithStringCode(c, http.StatusForbidden, "VERIFICATION_REQUIRED", "请先完成身份认证")
return
}
zap.L().Info("WebSocket connection attempt",
zap.String("user_id", userID),
)

View File

@@ -1,14 +1,16 @@
package middleware
import (
"github.com/gin-gonic/gin"
"net/http"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/repository"
"github.com/gin-gonic/gin"
)
func RequireVerification(userRepo repository.UserRepository) gin.HandlerFunc {
func RequireVerified(userRepo repository.UserRepository) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
@@ -19,13 +21,13 @@ func RequireVerification(userRepo repository.UserRepository) gin.HandlerFunc {
user, err := userRepo.GetByID(userID.(string))
if err != nil {
response.HandleError(c, err, "获取用户信息失败")
response.InternalServerError(c, "获取用户信息失败")
c.Abort()
return
}
if user.VerificationStatus != model.VerificationStatusApproved {
response.Forbidden(c, "需要完成身份认证")
response.ErrorWithStringCode(c, http.StatusForbidden, "VERIFICATION_REQUIRED", "请先完成身份认证")
c.Abort()
return
}

View File

@@ -184,9 +184,9 @@ func statusCodeFromAppErrorCode(code string) int {
// ErrorWithStringCode 带字符串错误码的错误响应
func ErrorWithStringCode(c *gin.Context, statusCode int, code string, message string) {
c.JSON(statusCode, ResponseSnakeCase{
Code: statusCode,
Message: message,
Data: nil,
c.JSON(statusCode, gin.H{
"code": statusCode,
"message": message,
"error_code": code,
})
}

View File

@@ -6,12 +6,14 @@ import (
"carrot_bbs/internal/handler"
"carrot_bbs/internal/middleware"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service"
)
// Router 路由配置
type Router struct {
engine *gin.Engine
userRepo repository.UserRepository
userHandler *handler.UserHandler
postHandler *handler.PostHandler
commentHandler *handler.CommentHandler
@@ -47,6 +49,7 @@ type Router struct {
// New 创建路由
func New(
userRepo repository.UserRepository,
userHandler *handler.UserHandler,
postHandler *handler.PostHandler,
commentHandler *handler.CommentHandler,
@@ -87,6 +90,7 @@ func New(
r := &Router{
engine: gin.Default(),
userRepo: userRepo,
userHandler: userHandler,
postHandler: postHandler,
commentHandler: commentHandler,
@@ -249,9 +253,9 @@ func (r *Router) setupRoutes() {
posts.GET("", middleware.OptionalAuth(r.jwtService), r.postHandler.List)
posts.GET("/search", middleware.OptionalAuth(r.jwtService), r.postHandler.Search)
posts.GET("/:id", middleware.OptionalAuth(r.jwtService), r.postHandler.GetByID)
posts.POST("", authMiddleware, r.postHandler.Create)
posts.PUT("/:id", authMiddleware, r.postHandler.Update)
posts.DELETE("/:id", authMiddleware, r.postHandler.Delete)
posts.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Create)
posts.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Update)
posts.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Delete)
// 浏览量记录(可选认证,允许游客浏览)
posts.POST("/:id/view", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordView)
@@ -259,18 +263,18 @@ func (r *Router) setupRoutes() {
posts.POST("/:id/share", middleware.OptionalAuth(r.jwtService), r.postHandler.RecordShare)
// 点赞
posts.POST("/:id/like", authMiddleware, r.postHandler.Like)
posts.DELETE("/:id/like", authMiddleware, r.postHandler.Unlike)
posts.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Like)
posts.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unlike)
// 收藏
posts.POST("/:id/favorite", authMiddleware, r.postHandler.Favorite)
posts.DELETE("/:id/favorite", authMiddleware, r.postHandler.Unfavorite)
posts.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Favorite)
posts.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.postHandler.Unfavorite)
// 投票相关路由
posts.POST("/vote", authMiddleware, r.voteHandler.CreateVotePost) // 创建投票帖子
posts.GET("/:id/vote", middleware.OptionalAuth(r.jwtService), r.voteHandler.GetVoteResult) // 获取投票结果
posts.POST("/:id/vote", authMiddleware, r.voteHandler.Vote) // 投票
posts.DELETE("/:id/vote", authMiddleware, r.voteHandler.Unvote) // 取消投票
posts.POST("/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.CreateVotePost) // 创建投票帖子
posts.GET("/:id/vote", middleware.OptionalAuth(r.jwtService), r.voteHandler.GetVoteResult) // 获取投票结果
posts.POST("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Vote) // 投票
posts.DELETE("/:id/vote", authMiddleware, middleware.RequireVerified(r.userRepo), r.voteHandler.Unvote) // 取消投票
}
// 频道路由(公开)
@@ -322,15 +326,15 @@ func (r *Router) setupRoutes() {
comments.GET("/post/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByPostID)
comments.GET("/post/:id/cursor", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByPostIDByCursor) // 帖子评论游标分页
comments.GET("/:id", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetByID)
comments.POST("", authMiddleware, r.commentHandler.Create)
comments.PUT("/:id", authMiddleware, r.commentHandler.Update)
comments.DELETE("/:id", authMiddleware, r.commentHandler.Delete)
comments.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Create)
comments.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Update)
comments.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Delete)
comments.GET("/:id/replies", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetReplies)
comments.GET("/:id/replies/flat", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetRepliesByRootID) // 扁平化分页获取回复
comments.GET("/:id/replies/cursor", middleware.OptionalAuth(r.jwtService), r.commentHandler.GetRepliesByRootIDByCursor) // 回复游标分页
// 评论点赞
comments.POST("/:id/like", authMiddleware, r.commentHandler.Like)
comments.DELETE("/:id/like", authMiddleware, r.commentHandler.Unlike)
comments.POST("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Like)
comments.DELETE("/:id/like", authMiddleware, middleware.RequireVerified(r.userRepo), r.commentHandler.Unlike)
}
// 会话路由
@@ -340,18 +344,18 @@ func (r *Router) setupRoutes() {
// ================================================================
// 新的 RESTful 风格路由(推荐使用)
// ================================================================
conversations.GET("", r.messageHandler.HandleGetConversationList) // 列表
conversations.GET("/cursor", r.messageHandler.GetConversationsByCursor) // 会话列表游标分页
conversations.POST("", r.messageHandler.HandleCreateConversation) // 创建
conversations.GET("/:id", r.messageHandler.HandleGetConversation) // 详情
conversations.GET("/:id/messages", r.messageHandler.HandleGetMessages) // 消息列表
conversations.GET("/:id/messages/cursor", r.messageHandler.GetMessagesByCursor) // 消息列表游标分页
conversations.POST("/:id/messages", r.messageHandler.HandleSendMessage) // 发送消息
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态
conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己)
conversations.GET("", r.messageHandler.HandleGetConversationList) // 列表
conversations.GET("/cursor", r.messageHandler.GetConversationsByCursor) // 会话列表游标分页
conversations.POST("", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleCreateConversation) // 创建
conversations.GET("/:id", r.messageHandler.HandleGetConversation) // 详情
conversations.GET("/:id/messages", r.messageHandler.HandleGetMessages) // 消息列表
conversations.GET("/:id/messages/cursor", r.messageHandler.GetMessagesByCursor) // 消息列表游标分页
conversations.POST("/:id/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息
conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读
conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置
conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数
conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态
conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己)
}
@@ -376,7 +380,7 @@ func (r *Router) setupRoutes() {
messages.Use(authMiddleware)
{
// 撤回/删除消息(统一接口)
messages.POST("/delete_msg", r.messageHandler.HandleDeleteMsg)
messages.POST("/delete_msg", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleDeleteMsg)
}
// 举报路由

View File

@@ -85,8 +85,9 @@ func ProvideWSHandler(
groupService service.GroupService,
jwtService *service.JWTService,
callService service.CallService,
userRepo repository.UserRepository,
) *handler.WSHandler {
return handler.NewWSHandler(wsHub, chatService, groupService, jwtService, callService)
return handler.NewWSHandler(wsHub, chatService, groupService, jwtService, callService, userRepo)
}
// ProvideAdminVerificationHandler 提供管理端身份认证处理器