package controllers import ( "strconv" "abdanhafidz.com/go-boilerplate/models/dto" entity "abdanhafidz.com/go-boilerplate/models/entity" http_error "abdanhafidz.com/go-boilerplate/models/error" "abdanhafidz.com/go-boilerplate/services" "github.com/gin-gonic/gin" ) type SuperAdminRoleController interface { ListRoles(ctx *gin.Context) CreateRole(ctx *gin.Context) UpdateRole(ctx *gin.Context) DeleteRole(ctx *gin.Context) ListAuditLogs(ctx *gin.Context) } type superAdminRoleController struct { accountService services.AccountService } func NewSuperAdminRoleController(accountService services.AccountService) SuperAdminRoleController { return &superAdminRoleController{accountService: accountService} } // ListRoles godoc // @Summary List All Roles // @Description Get all system and named custom roles with their privilege flags // @Tags Super Admin Roles // @Accept json // @Produce json // @Security BearerAuth // @Success 200 {object} dto.SuccessResponse[[]dto.RoleResponse] // @Failure 401 {object} dto.ErrorResponse // @Failure 403 {object} dto.ErrorResponse // @Router /api/v1/super-admin/roles [get] func (c *superAdminRoleController) ListRoles(ctx *gin.Context) { roles, err := c.accountService.GetAllRoles(ctx.Request.Context()) if err != nil { ResponseJSON(ctx, gin.H{}, []dto.RoleResponse{}, err) return } var res []dto.RoleResponse for _, r := range roles { res = append(res, dto.RoleResponse{ Id: r.Id, Name: r.Name, IsSystem: r.IsSystem, EventManagement: r.EventManagement, ExamManagement: r.ExamManagement, AcademyManagement: r.AcademyManagement, ContentManagement: r.ContentManagement, }) } ResponseJSON(ctx, gin.H{}, res, nil) } // CreateRole godoc // @Summary Create Named Role // @Description Create a new named custom role with privilege flags. Superadmin only. // @Tags Super Admin Roles // @Accept json // @Produce json // @Security BearerAuth // @Param request body dto.CreateRoleRequest true "Create Role Request" // @Success 200 {object} dto.SuccessResponse[dto.RoleResponse] // @Failure 400 {object} dto.ErrorResponse // @Failure 401 {object} dto.ErrorResponse // @Failure 403 {object} dto.ErrorResponse // @Failure 409 {object} dto.ErrorResponse // @Router /api/v1/super-admin/roles [post] func (c *superAdminRoleController) CreateRole(ctx *gin.Context) { req := RequestJSON[dto.CreateRoleRequest](ctx) role := entity.Role{ Name: req.Name, IsSystem: false, EventManagement: req.EventManagement, ExamManagement: req.ExamManagement, AcademyManagement: req.AcademyManagement, ContentManagement: req.ContentManagement, } created, err := c.accountService.CreateRole(ctx.Request.Context(), role) if err != nil { ResponseJSON(ctx, req, dto.RoleResponse{}, err) return } callerID := ParseAccountId(ctx) roleIDStr := created.Id.String() detailStr := `{"role_name":"` + created.Name + `"}` _ = c.accountService.WriteAuditLog(ctx.Request.Context(), callerID, "role_created", &roleIDStr, &detailStr) res := dto.RoleResponse{ Id: created.Id, Name: created.Name, IsSystem: created.IsSystem, EventManagement: created.EventManagement, ExamManagement: created.ExamManagement, AcademyManagement: created.AcademyManagement, ContentManagement: created.ContentManagement, } ResponseJSON(ctx, req, res, nil) } // UpdateRole godoc // @Summary Update Named Role // @Description Update a named custom role's privilege flags. System roles are immutable. // @Tags Super Admin Roles // @Accept json // @Produce json // @Security BearerAuth // @Param role_id path string true "Role ID" // @Param request body dto.UpdateRoleRequest true "Update Role Request" // @Success 200 {object} dto.SuccessResponse[dto.RoleResponse] // @Failure 400 {object} dto.ErrorResponse // @Failure 401 {object} dto.ErrorResponse // @Failure 403 {object} dto.ErrorResponse // @Router /api/v1/super-admin/roles/{role_id} [put] func (c *superAdminRoleController) UpdateRole(ctx *gin.Context) { roleID := ParseUUID(ctx, "role_id") req := RequestJSON[dto.UpdateRoleRequest](ctx) existing, err := c.accountService.GetRoleByID(ctx.Request.Context(), roleID) if err != nil { ResponseJSON(ctx, req, dto.RoleResponse{}, err) return } if existing.IsSystem { ResponseJSON(ctx, req, dto.RoleResponse{}, http_error.ROLE_IMMUTABLE) return } if req.Name != nil { existing.Name = *req.Name } if req.EventManagement != nil { existing.EventManagement = *req.EventManagement } if req.ExamManagement != nil { existing.ExamManagement = *req.ExamManagement } if req.AcademyManagement != nil { existing.AcademyManagement = *req.AcademyManagement } if req.ContentManagement != nil { existing.ContentManagement = *req.ContentManagement } updated, err := c.accountService.UpdateRole(ctx.Request.Context(), existing) if err != nil { ResponseJSON(ctx, req, dto.RoleResponse{}, err) return } callerID := ParseAccountId(ctx) roleIDStr := updated.Id.String() detailStr := `{"role_name":"` + updated.Name + `"}` _ = c.accountService.WriteAuditLog(ctx.Request.Context(), callerID, "role_updated", &roleIDStr, &detailStr) res := dto.RoleResponse{ Id: updated.Id, Name: updated.Name, IsSystem: updated.IsSystem, EventManagement: updated.EventManagement, ExamManagement: updated.ExamManagement, AcademyManagement: updated.AcademyManagement, ContentManagement: updated.ContentManagement, } ResponseJSON(ctx, req, res, nil) } // DeleteRole godoc // @Summary Delete Named Role // @Description Delete a named custom role. System roles are immutable. Cannot delete if assigned to any account. // @Tags Super Admin Roles // @Accept json // @Produce json // @Security BearerAuth // @Param role_id path string true "Role ID" // @Success 200 {object} dto.SuccessResponse[any] // @Failure 401 {object} dto.ErrorResponse // @Failure 403 {object} dto.ErrorResponse // @Failure 409 {object} dto.ErrorResponse // @Router /api/v1/super-admin/roles/{role_id} [delete] func (c *superAdminRoleController) DeleteRole(ctx *gin.Context) { roleID := ParseUUID(ctx, "role_id") existing, err := c.accountService.GetRoleByID(ctx.Request.Context(), roleID) if err != nil { ResponseJSON[any](ctx, gin.H{}, nil, err) return } if existing.IsSystem { ResponseJSON[any](ctx, gin.H{}, nil, http_error.ROLE_IMMUTABLE) return } inUse, err := c.accountService.CountRoleAssignments(ctx.Request.Context(), roleID) if err != nil { ResponseJSON[any](ctx, gin.H{}, nil, err) return } if inUse > 0 { ResponseJSON[any](ctx, gin.H{}, nil, http_error.ROLE_IN_USE) return } if err := c.accountService.DeleteRole(ctx.Request.Context(), roleID); err != nil { ResponseJSON[any](ctx, gin.H{}, nil, err) return } callerID := ParseAccountId(ctx) roleIDStr := roleID.String() detailStr := `{"role_name":"` + existing.Name + `"}` _ = c.accountService.WriteAuditLog(ctx.Request.Context(), callerID, "role_deleted", &roleIDStr, &detailStr) ResponseJSON(ctx, gin.H{}, gin.H{"message": "Role deleted successfully"}, nil) } // ListAuditLogs godoc // @Summary List Audit Logs // @Description Get paginated audit logs with optional filters. Superadmin only. // @Tags Super Admin Audit // @Accept json // @Produce json // @Security BearerAuth // @Param page query int false "Page number (default 1)" // @Param limit query int false "Items per page (default 10, max 50)" // @Param action query string false "Filter by action" // @Param actor_account_id query string false "Filter by actor account ID" // @Param from query string false "Start time (RFC3339)" // @Param to query string false "End time (RFC3339)" // @Success 200 {object} dto.SuccessResponse[[]dto.AuditLogResponse] // @Failure 401 {object} dto.ErrorResponse // @Failure 403 {object} dto.ErrorResponse // @Router /api/v1/super-admin/logs [get] func (c *superAdminRoleController) ListAuditLogs(ctx *gin.Context) { limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10")) page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1")) action := ctx.DefaultQuery("action", "") actorID := ctx.DefaultQuery("actor_account_id", "") from := ctx.DefaultQuery("from", "") to := ctx.DefaultQuery("to", "") if limit < 1 { limit = 10 } else if limit > 50 { limit = 50 } if page < 1 { page = 1 } logs, total, err := c.accountService.GetAuditLogs(ctx.Request.Context(), page, limit, action, actorID, from, to) if err != nil { ResponseJSON(ctx, gin.H{}, []dto.AuditLogResponse{}, err) return } var res []dto.AuditLogResponse for _, l := range logs { res = append(res, dto.AuditLogResponse{ Id: l.Id, ActorAccountID: l.ActorAccountID, Action: l.Action, TargetID: l.TargetID, Detail: l.Detail, CreatedAt: l.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), }) } var totalPages int if total == 0 { totalPages = 1 } else { totalPages = int((total + int64(limit) - 1) / int64(limit)) } if page > totalPages { page = totalPages } meta := gin.H{ "totalItems": total, "totalPages": totalPages, "currentPage": page, "limit": limit, } ResponseJSON(ctx, meta, res, nil) }