Yimaru-BackEnd/internal/web_server/handlers/rbac_handler.go

566 lines
18 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"context"
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// ListRoles godoc
// @Summary List all roles
// @Description Get all roles with optional filters
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param query query string false "Search by role name"
// @Param is_system query bool false "Filter by system role (true/false)"
// @Param page query int false "Page number (default: 1)"
// @Param page_size query int false "Page size (default: 20)"
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles [get]
func (h *Handler) ListRoles(c *fiber.Ctx) error {
filter := domain.RoleListFilter{
Query: c.Query("query"),
Page: int64(c.QueryInt("page", 1) - 1),
PageSize: int64(c.QueryInt("page_size", 20)),
}
if isSystemStr := c.Query("is_system"); isSystemStr != "" {
isSystem := isSystemStr == "true"
filter.IsSystem = &isSystem
}
roles, total, err := h.rbacSvc.ListRoles(c.Context(), filter)
if err != nil {
h.mongoLoggerSvc.Error("Failed to list roles",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list roles",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Roles retrieved successfully",
Data: map[string]interface{}{
"roles": roles,
"total": total,
"page": filter.Page + 1,
"page_size": filter.PageSize,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetRoleByID godoc
// @Summary Get a role by ID
// @Description Get a role and its permissions by ID
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Role ID"
// @Success 200 {object} domain.Response{data=domain.RoleWithPermissions}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles/{id} [get]
func (h *Handler) GetRoleByID(c *fiber.Ctx) error {
roleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil || roleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role ID",
Error: "Role ID must be a valid positive integer",
})
}
role, err := h.rbacSvc.GetRoleByID(c.Context(), roleID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get role",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get role",
Error: err.Error(),
})
}
permissions, err := h.rbacSvc.GetRolePermissions(c.Context(), roleID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get role permissions",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get role permissions",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Role retrieved successfully",
Data: domain.RoleWithPermissions{
RoleRecord: role,
Permissions: permissions,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// CreateRole godoc
// @Summary Create a new role
// @Description Create a new role with a name and description
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param body body domain.CreateRoleReq true "Role creation payload"
// @Success 201 {object} domain.Response{data=domain.RoleRecord}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles [post]
func (h *Handler) CreateRole(c *fiber.Ctx) error {
var req domain.CreateRoleReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse CreateRole request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to create role",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to create role",
Error: errMsg,
})
}
role, err := h.rbacSvc.CreateRole(c.Context(), req.Name, req.Description)
if err != nil {
h.mongoLoggerSvc.Error("Failed to create role",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create role",
Error: err.Error(),
})
}
h.mongoLoggerSvc.Info("Role created successfully",
zap.Int("status_code", fiber.StatusCreated),
zap.Int64("role_id", role.ID),
zap.Time("timestamp", time.Now()),
)
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"name": req.Name, "description": req.Description})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionRoleCreated, domain.ResourceRole, &role.ID, "Created role: "+req.Name, meta, &ip, &ua)
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Role created successfully",
Data: role,
Success: true,
StatusCode: fiber.StatusCreated,
})
}
// UpdateRole godoc
// @Summary Update a role
// @Description Update an existing role's name and description
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Role ID"
// @Param body body domain.UpdateRoleReq true "Role update payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles/{id} [put]
func (h *Handler) UpdateRole(c *fiber.Ctx) error {
roleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil || roleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role ID",
Error: "Role ID must be a valid positive integer",
})
}
var req domain.UpdateRoleReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse UpdateRole request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update role",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to update role",
Error: errMsg,
})
}
if err := h.rbacSvc.UpdateRole(c.Context(), roleID, req.Name, req.Description); err != nil {
h.mongoLoggerSvc.Error("Failed to update role",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update role",
Error: err.Error(),
})
}
h.mongoLoggerSvc.Info("Role updated successfully",
zap.Int64("role_id", roleID),
zap.Time("timestamp", time.Now()),
)
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"role_id": roleID, "name": req.Name, "description": req.Description})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionRoleUpdated, domain.ResourceRole, &roleID, fmt.Sprintf("Updated role ID: %d", roleID), meta, &ip, &ua)
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Role updated successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// DeleteRole godoc
// @Summary Delete a role
// @Description Delete a non-system role by ID
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Role ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles/{id} [delete]
func (h *Handler) DeleteRole(c *fiber.Ctx) error {
roleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil || roleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role ID",
Error: "Role ID must be a valid positive integer",
})
}
if err := h.rbacSvc.DeleteRole(c.Context(), roleID); err != nil {
h.mongoLoggerSvc.Error("Failed to delete role",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete role",
Error: err.Error(),
})
}
h.mongoLoggerSvc.Info("Role deleted successfully",
zap.Int64("role_id", roleID),
zap.Time("timestamp", time.Now()),
)
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"role_id": roleID})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionRoleDeleted, domain.ResourceRole, &roleID, fmt.Sprintf("Deleted role ID: %d", roleID), meta, &ip, &ua)
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Role deleted successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// SetRolePermissions godoc
// @Summary Set permissions for a role
// @Description Replace all permissions for a role with the given permission IDs
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Role ID"
// @Param body body domain.SetRolePermissionsReq true "Permission IDs payload"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles/{id}/permissions [put]
func (h *Handler) SetRolePermissions(c *fiber.Ctx) error {
roleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil || roleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role ID",
Error: "Role ID must be a valid positive integer",
})
}
var req domain.SetRolePermissionsReq
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse SetRolePermissions request",
zap.Int("status_code", fiber.StatusBadRequest),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to set role permissions",
Error: "Invalid request body: " + err.Error(),
})
}
if valErrs, ok := h.validator.Validate(c, req); !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Failed to set role permissions",
Error: errMsg,
})
}
if err := h.rbacSvc.SetRolePermissions(c.Context(), roleID, req.PermissionIDs); err != nil {
h.mongoLoggerSvc.Error("Failed to set role permissions",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to set role permissions",
Error: err.Error(),
})
}
h.mongoLoggerSvc.Info("Role permissions set successfully",
zap.Int64("role_id", roleID),
zap.Time("timestamp", time.Now()),
)
actorID := c.Locals("user_id").(int64)
actorRole := string(c.Locals("role").(domain.Role))
ip := c.IP()
ua := c.Get("User-Agent")
meta, _ := json.Marshal(map[string]interface{}{"role_id": roleID, "permission_ids": req.PermissionIDs})
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionRolePermissionsSet, domain.ResourceRole, &roleID, fmt.Sprintf("Set permissions for role ID: %d", roleID), meta, &ip, &ua)
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Role permissions set successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetRolePermissions godoc
// @Summary Get permissions for a role
// @Description Get all permissions assigned to a role
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Param id path int true "Role ID"
// @Success 200 {object} domain.Response{data=[]domain.Permission}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/roles/{id}/permissions [get]
func (h *Handler) GetRolePermissions(c *fiber.Ctx) error {
roleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil || roleID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid role ID",
Error: "Role ID must be a valid positive integer",
})
}
permissions, err := h.rbacSvc.GetRolePermissions(c.Context(), roleID)
if err != nil {
h.mongoLoggerSvc.Error("Failed to get role permissions",
zap.Int64("role_id", roleID),
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to get role permissions",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Role permissions retrieved successfully",
Data: permissions,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// ListPermissions godoc
// @Summary List all permissions
// @Description Get all permissions in the system grouped by group name
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} domain.Response{data=[]domain.Permission}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/permissions [get]
func (h *Handler) ListPermissions(c *fiber.Ctx) error {
permissions, err := h.rbacSvc.ListPermissions(c.Context())
if err != nil {
h.mongoLoggerSvc.Error("Failed to list permissions",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list permissions",
Error: err.Error(),
})
}
grouped := make(map[string][]domain.Permission)
for _, p := range permissions {
grouped[p.GroupName] = append(grouped[p.GroupName], p)
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Permissions retrieved successfully",
Data: grouped,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// ListPermissionGroups godoc
// @Summary List permission groups
// @Description Get all distinct permission group names
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} domain.Response{data=[]string}
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/permissions/groups [get]
func (h *Handler) ListPermissionGroups(c *fiber.Ctx) error {
groups, err := h.rbacSvc.ListPermissionGroups(c.Context())
if err != nil {
h.mongoLoggerSvc.Error("Failed to list permission groups",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list permission groups",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Permission groups retrieved successfully",
Data: groups,
Success: true,
StatusCode: fiber.StatusOK,
})
}
// SyncPermissions godoc
// @Summary Sync permissions
// @Description Re-seed permissions from code and reload the RBAC cache
// @Tags rbac
// @Accept json
// @Produce json
// @Security Bearer
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/rbac/permissions/sync [post]
func (h *Handler) SyncPermissions(c *fiber.Ctx) error {
if err := h.rbacSvc.SeedPermissions(c.Context()); err != nil {
h.mongoLoggerSvc.Error("Failed to seed permissions",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to sync permissions",
Error: err.Error(),
})
}
if err := h.rbacSvc.Reload(c.Context()); err != nil {
h.mongoLoggerSvc.Error("Failed to reload RBAC cache",
zap.Int("status_code", fiber.StatusInternalServerError),
zap.Error(err),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to reload RBAC cache",
Error: err.Error(),
})
}
h.mongoLoggerSvc.Info("Permissions synced and cache reloaded",
zap.Int("status_code", fiber.StatusOK),
zap.Time("timestamp", time.Now()),
)
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Permissions synced and cache reloaded successfully",
Success: true,
StatusCode: fiber.StatusOK,
})
}