feat(auth): add Casbin RBAC and user activity tracking
Integrate Casbin for role-based access control with admin routes for role management. Add user activity logging service to track login and refresh events. Refactor audit service to use AI-based content moderation.
This commit is contained in:
204
internal/handler/role_handler.go
Normal file
204
internal/handler/role_handler.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RoleHandler 角色管理处理器
|
||||
type RoleHandler struct {
|
||||
roleService service.RoleService
|
||||
}
|
||||
|
||||
// NewRoleHandler 创建角色处理器
|
||||
func NewRoleHandler(roleService service.RoleService) *RoleHandler {
|
||||
return &RoleHandler{
|
||||
roleService: roleService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllRoles 获取所有角色
|
||||
// GET /api/v1/admin/roles
|
||||
func (h *RoleHandler) GetAllRoles(c *gin.Context) {
|
||||
roles, err := h.roleService.GetAllRoles(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": "获取角色列表失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": h.toRoleListResponse(roles),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserRoles 获取用户的角色
|
||||
// GET /api/v1/admin/users/:id/roles
|
||||
func (h *RoleHandler) GetUserRoles(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "缺少用户ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
roles, err := h.roleService.GetUserRoles(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": "获取用户角色失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": h.toRoleListResponse(roles),
|
||||
})
|
||||
}
|
||||
|
||||
// AssignRoleRequest 分配角色请求
|
||||
type AssignRoleRequest struct {
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
|
||||
// AssignRole 为用户分配角色
|
||||
// POST /api/v1/admin/users/:id/roles
|
||||
func (h *RoleHandler) AssignRole(c *gin.Context) {
|
||||
targetUserID := c.Param("id")
|
||||
if targetUserID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "缺少用户ID",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req AssignRoleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "请求参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取操作者ID
|
||||
operatorID, _ := c.Get("user_id")
|
||||
|
||||
err := h.roleService.AssignRole(c.Request.Context(), operatorID.(string), targetUserID, req.Role)
|
||||
if err != nil {
|
||||
h.handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "角色分配成功",
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveRole 移除用户角色
|
||||
// DELETE /api/v1/admin/users/:id/roles/:role
|
||||
func (h *RoleHandler) RemoveRole(c *gin.Context) {
|
||||
targetUserID := c.Param("id")
|
||||
role := c.Param("role")
|
||||
|
||||
if targetUserID == "" || role == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "缺少用户ID或角色名",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取操作者ID
|
||||
operatorID, _ := c.Get("user_id")
|
||||
|
||||
err := h.roleService.RemoveRole(c.Request.Context(), operatorID.(string), targetUserID, role)
|
||||
if err != nil {
|
||||
h.handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "角色移除成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetMyRoles 获取当前用户的角色
|
||||
// GET /api/v1/users/me/roles
|
||||
func (h *RoleHandler) GetMyRoles(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
roles, err := h.roleService.GetUserRoles(c.Request.Context(), userID.(string))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": "获取用户角色失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": h.toRoleListResponse(roles),
|
||||
})
|
||||
}
|
||||
|
||||
// toRoleListResponse 转换为响应格式
|
||||
func (h *RoleHandler) toRoleListResponse(roles []model.Role) []gin.H {
|
||||
result := make([]gin.H, len(roles))
|
||||
for i, role := range roles {
|
||||
result[i] = gin.H{
|
||||
"name": role.Name,
|
||||
"display_name": role.DisplayName,
|
||||
"description": role.Description,
|
||||
"priority": role.Priority,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// handleError 处理错误
|
||||
func (h *RoleHandler) handleError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, apperrors.ErrCannotModifyOwnRole):
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": "CANNOT_MODIFY_OWN_ROLE",
|
||||
"message": "不能修改自己的角色",
|
||||
})
|
||||
case errors.Is(err, apperrors.ErrCannotModifySuperAdmin):
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": "CANNOT_MODIFY_SUPER_ADMIN",
|
||||
"message": "不能修改超级管理员角色",
|
||||
})
|
||||
case errors.Is(err, apperrors.ErrRoleAlreadyAssigned):
|
||||
c.JSON(http.StatusConflict, gin.H{
|
||||
"code": "ROLE_ALREADY_ASSIGNED",
|
||||
"message": "用户已拥有该角色",
|
||||
})
|
||||
case errors.Is(err, apperrors.ErrRoleNotAssigned):
|
||||
c.JSON(http.StatusConflict, gin.H{
|
||||
"code": "ROLE_NOT_ASSIGNED",
|
||||
"message": "用户未拥有该角色",
|
||||
})
|
||||
case errors.Is(err, apperrors.ErrRoleNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": "ROLE_NOT_FOUND",
|
||||
"message": "角色不存在",
|
||||
})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": "操作失败",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
"carrot_bbs/internal/service"
|
||||
)
|
||||
|
||||
// UserHandler 用户处理器
|
||||
type UserHandler struct {
|
||||
userService service.UserService
|
||||
jwtService *service.JWTService
|
||||
userService service.UserService
|
||||
activityService service.UserActivityService // 用户活跃统计服务
|
||||
jwtService *service.JWTService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||
func NewUserHandler(userService service.UserService, activityService service.UserActivityService) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
userService: userService,
|
||||
activityService: activityService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +94,17 @@ func (h *UserHandler) Login(c *gin.Context) {
|
||||
accessToken, _ := h.jwtService.GenerateAccessToken(user.ID, user.Username)
|
||||
refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID, user.Username)
|
||||
|
||||
// 记录用户活跃
|
||||
if h.activityService != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
loginType := model.LoginTypeLogin
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
h.activityService.RecordUserActive(ctx, user.ID, loginType, ip, userAgent)
|
||||
}()
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"user": dto.ConvertUserToResponse(user),
|
||||
"token": accessToken,
|
||||
@@ -353,6 +368,17 @@ func (h *UserHandler) RefreshToken(c *gin.Context) {
|
||||
accessToken, _ := h.jwtService.GenerateAccessToken(claims.UserID, claims.Username)
|
||||
refreshToken, _ := h.jwtService.GenerateRefreshToken(claims.UserID, claims.Username)
|
||||
|
||||
// 记录用户活跃
|
||||
if h.activityService != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
loginType := model.LoginTypeRefresh
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
h.activityService.RecordUserActive(ctx, claims.UserID, loginType, ip, userAgent)
|
||||
}()
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{
|
||||
"token": accessToken,
|
||||
"refresh_token": refreshToken,
|
||||
@@ -364,6 +390,11 @@ func (h *UserHandler) SetJWTService(jwtSvc *service.JWTService) {
|
||||
h.jwtService = jwtSvc
|
||||
}
|
||||
|
||||
// SetActivityService 设置活跃统计服务
|
||||
func (h *UserHandler) SetActivityService(activityService service.UserActivityService) {
|
||||
h.activityService = activityService
|
||||
}
|
||||
|
||||
// FollowUser 关注用户
|
||||
func (h *UserHandler) FollowUser(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
Reference in New Issue
Block a user