Files
RenewCMS/api/handlers/role/roleHandler.go
T

252 lines
7.8 KiB
Go

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.ErrReassignTargetNotFound, http.StatusConflict, apperr.ReassignTargetNotFoundErrorMessage},
{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})
}