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

View File

@@ -132,8 +132,8 @@ func InitializeApp() (*App, error) {
verificationHandler := handler.NewVerificationHandler(verificationService) verificationHandler := handler.NewVerificationHandler(verificationService)
adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository) adminVerificationService := wire.ProvideAdminVerificationService(verificationRepository, userRepository)
adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository) adminVerificationHandler := wire.ProvideAdminVerificationHandler(adminVerificationService, userRepository)
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService) wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService, userRepository)
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) 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) hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, cache)
app := NewApp(config, db, router, pushService, hotRankWorker, server, logger) app := NewApp(config, db, router, pushService, hotRankWorker, server, logger)
return app, nil return app, nil
@@ -143,6 +143,7 @@ func InitializeApp() (*App, error) {
// ProvideRouter 提供路由 // ProvideRouter 提供路由
func ProvideRouter( func ProvideRouter(
userRepo repository.UserRepository,
userHandler *handler.UserHandler, userHandler *handler.UserHandler,
postHandler *handler.PostHandler, postHandler *handler.PostHandler,
commentHandler *handler.CommentHandler, commentHandler *handler.CommentHandler,
@@ -177,6 +178,7 @@ func ProvideRouter(
wsHandler *handler.WSHandler, wsHandler *handler.WSHandler,
) *router.Router { ) *router.Router {
return router.New( return router.New(
userRepo,
userHandler, userHandler,
postHandler, postHandler,
commentHandler, commentHandler,

View File

@@ -20,6 +20,7 @@ import (
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response" "carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/pkg/ws" "carrot_bbs/internal/pkg/ws"
"carrot_bbs/internal/repository"
"carrot_bbs/internal/service" "carrot_bbs/internal/service"
) )
@@ -71,6 +72,7 @@ type WSHandler struct {
groupService service.GroupService groupService service.GroupService
jwtService *service.JWTService jwtService *service.JWTService
callService service.CallService callService service.CallService
userRepo repository.UserRepository
clientSeq uint64 clientSeq uint64
} }
@@ -81,6 +83,7 @@ func NewWSHandler(
groupService service.GroupService, groupService service.GroupService,
jwtService *service.JWTService, jwtService *service.JWTService,
callService service.CallService, callService service.CallService,
userRepo repository.UserRepository,
) *WSHandler { ) *WSHandler {
return &WSHandler{ return &WSHandler{
wsHub: wsHub, wsHub: wsHub,
@@ -88,6 +91,7 @@ func NewWSHandler(
groupService: groupService, groupService: groupService,
jwtService: jwtService, jwtService: jwtService,
callService: callService, callService: callService,
userRepo: userRepo,
} }
} }
@@ -118,6 +122,18 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
} }
userID := claims.UserID 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.L().Info("WebSocket connection attempt",
zap.String("user_id", userID), zap.String("user_id", userID),
) )

View File

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

View File

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

View File

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