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, }) }