250 lines
7.6 KiB
Go
250 lines
7.6 KiB
Go
package role
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/access"
|
|
"RenewCMS/api/pkg/apperr"
|
|
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, 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, 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, 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, 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, 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})
|
|
}
|