package role import ( "RenewCMS/api/pkg/access" "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/payload" validator "RenewCMS/api/pkg/validator" domain "RenewCMS/internal/domain/role" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "log" "net/http" "strconv" "github.com/go-chi/chi/v5" ) type Handler struct { ListUseCase *useCases.ListRolesUseCase GetUseCase *useCases.GetRoleUseCase CreateUseCase *useCases.CreateRoleUseCase UpdateUseCase *useCases.UpdateRoleUseCase DeleteUseCase *useCases.DeleteRoleUseCase ReassignUseCase *useCases.ReassignRoleMembersUseCase } func NewHandler(list *useCases.ListRolesUseCase, get *useCases.GetRoleUseCase, create *useCases.CreateRoleUseCase, update *useCases.UpdateRoleUseCase, deleteUseCase *useCases.DeleteRoleUseCase, reassign *useCases.ReassignRoleMembersUseCase) *Handler { return &Handler{ListUseCase: list, GetUseCase: get, CreateUseCase: create, UpdateUseCase: update, DeleteUseCase: deleteUseCase, ReassignUseCase: reassign} } type GrantRequest struct { Key string `json:"key" validate:"required,max=100"` Scope string `json:"scope"` } type PostRole struct { Name string `json:"name" validate:"required,max=100"` Description string `json:"description" validate:"max=255"` Grants []GrantRequest `json:"grants" validate:"dive"` } type PutRole struct { Name string `json:"name" validate:"required,max=100"` Description string `json:"description" validate:"max=255"` Grants []GrantRequest `json:"grants" validate:"dive"` Version uint32 `json:"version" validate:"required,min=1"` } type ResetRole struct { Version uint32 `json:"version" validate:"required,min=1"` } type ReassignRole struct { TargetRoleID uint32 `json:"target_role_id" validate:"required"` } func toGrants(requested []GrantRequest) []domain.Grant { grants := make([]domain.Grant, 0, len(requested)) for _, grant := range requested { grants = append(grants, domain.Grant{Key: grant.Key, Scope: domain.Scope(grant.Scope)}) } return grants } func parseRoleId(r *http.Request) (uint32, error) { id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) return uint32(id), err } func decodeRequest(w http.ResponseWriter, r *http.Request, request any) bool { if err := json.NewDecoder(r.Body).Decode(request); err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return false } if err := validator.Validate.Struct(request); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return false } return true } func readId(w http.ResponseWriter, r *http.Request) (uint32, bool) { id, err := parseRoleId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return 0, false } return id, true } func writeJSON(w http.ResponseWriter, status int, payload any) { body, err := json.Marshal(payload) if err != nil { http.Error(w, "Failed to encode response", http.StatusInternalServerError) return } if status != http.StatusOK { w.WriteHeader(status) } if _, err := w.Write(body); err != nil { log.Printf("Failed to write response: %v", err) } } type refusal struct { cause error status int message string } var refusals = []refusal{ {domain.ErrRoleNotFound, http.StatusNotFound, apperr.RoleNotFoundErrorMessage}, {domain.ErrRoleLocked, http.StatusForbidden, apperr.RoleLockedErrorMessage}, {domain.ErrBuiltInRole, http.StatusConflict, apperr.RoleBuiltInErrorMessage}, {domain.ErrBuiltInName, http.StatusBadRequest, apperr.BuiltInNameErrorMessage}, {domain.ErrNotBuiltIn, http.StatusBadRequest, apperr.NotBuiltInErrorMessage}, {domain.ErrOwnRole, http.StatusForbidden, apperr.OwnRoleErrorMessage}, {domain.ErrRoleNameTaken, http.StatusConflict, apperr.RoleNameTakenErrorMessage}, {domain.ErrRoleConflict, http.StatusConflict, apperr.RoleConflictErrorMessage}, {domain.ErrRoleHasMembers, http.StatusConflict, apperr.RoleHasMembersErrorMessage}, {domain.ErrInvalidReassign, http.StatusBadRequest, apperr.InvalidReassignErrorMessage}, {domain.ErrInvalidRoleName, http.StatusBadRequest, apperr.InvalidRoleNameErrorMessage}, {domain.ErrUnknownPermission, http.StatusBadRequest, apperr.UnknownPermissionErrorMessage}, {domain.ErrInvalidScope, http.StatusBadRequest, apperr.InvalidScopeErrorMessage}, {domain.ErrOwnerOnlyGrant, http.StatusBadRequest, apperr.OwnerOnlyPermissionErrorMessage}, {domain.ErrDuplicateGrant, http.StatusBadRequest, apperr.DuplicatePermissionErrorMessage}, {domain.ErrUnknownContentType, http.StatusBadRequest, apperr.UnknownContentTypeErrorMessage}, } func writeRoleError(w http.ResponseWriter, err error) { if access.WriteIfMissingPermission(w, err) { return } var escalation domain.EscalationError if errors.As(err, &escalation) { w.Header().Set(apperr.EscalationHeader, escalation.Key) http.Error(w, apperr.EscalationErrorMessage, http.StatusForbidden) return } for _, candidate := range refusals { if errors.Is(err, candidate.cause) { http.Error(w, candidate.message, candidate.status) return } } http.Error(w, "Internal server error", http.StatusInternalServerError) } func (h *Handler) List(w http.ResponseWriter, _ *http.Request) { listed, err := h.ListUseCase.List() if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusOK, payload.RoleViewsOf(listed)) } func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { id, ok := readId(w, r) if !ok { return } found, err := h.GetUseCase.Get(id) if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusOK, payload.RoleViewOf(found)) } func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var request PostRole if !decodeRequest(w, r, &request) { return } created, err := h.CreateUseCase.Create(r.Context(), useCases.CreateRoleCommand{ Caller: access.CallerOf(r), Name: request.Name, Description: request.Description, Grants: toGrants(request.Grants), }) if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusCreated, payload.RoleViewOf(created)) } func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { id, ok := readId(w, r) if !ok { return } var request PutRole if !decodeRequest(w, r, &request) { return } updated, err := h.UpdateUseCase.Update(r.Context(), useCases.UpdateRoleCommand{ Caller: access.CallerOf(r), ID: id, Name: request.Name, Description: request.Description, Grants: toGrants(request.Grants), Version: request.Version, }) if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusOK, payload.RoleViewOf(updated)) } func (h *Handler) Reset(w http.ResponseWriter, r *http.Request) { id, ok := readId(w, r) if !ok { return } var request ResetRole if !decodeRequest(w, r, &request) { return } restored, err := h.UpdateUseCase.Reset(r.Context(), useCases.ResetRoleCommand{Caller: access.CallerOf(r), ID: id, Version: request.Version}) if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusOK, payload.RoleViewOf(restored)) } func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { id, ok := readId(w, r) if !ok { return } if err := h.DeleteUseCase.Delete(r.Context(), useCases.DeleteRoleCommand{Caller: access.CallerOf(r), ID: id}); err != nil { writeRoleError(w, err) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) Reassign(w http.ResponseWriter, r *http.Request) { id, ok := readId(w, r) if !ok { return } var request ReassignRole if !decodeRequest(w, r, &request) { return } moved, err := h.ReassignUseCase.Reassign(r.Context(), useCases.ReassignRoleMembersCommand{Caller: access.CallerOf(r), SourceID: id, TargetID: request.TargetRoleID}) if err != nil { writeRoleError(w, err) return } writeJSON(w, http.StatusOK, map[string]int{"moved": moved}) }