package user import ( "RenewCMS/api/pkg/access" "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/claims" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/invite" "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "encoding/json" "errors" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" ) type InviteHandler struct { CreateInviteUseCase *useCases.CreateInviteUseCase CancelInviteUseCase *useCases.CancelInviteUseCase RenewInviteUseCase *useCases.RenewInviteUseCase ListUsersAndInvitesUseCase *useCases.ListUsersAndInvitesUseCase } type CreateInviteRequest struct { Email string `json:"email" validate:"required,inviteemail"` RoleID uint32 `json:"role_id"` } type inviteResponse struct { ID uint32 `json:"id"` Kind string `json:"kind"` Email string `json:"email"` Status string `json:"status"` Role useCases.RoleRef `json:"role"` CreatedAt time.Time `json:"created_at"` ExpiresAt time.Time `json:"expires_at"` } type inviteListItem struct { ID uint32 `json:"id"` Kind string `json:"kind"` Email string `json:"email"` Role *useCases.RoleRef `json:"role"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` } type userListItem struct { ID uint32 `json:"id"` Kind string `json:"kind"` Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Role *useCases.RoleRef `json:"role"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` } func NewInviteHandler(createInvite *useCases.CreateInviteUseCase, cancelInvite *useCases.CancelInviteUseCase, renewInvite *useCases.RenewInviteUseCase, listUsersAndInvites *useCases.ListUsersAndInvitesUseCase) *InviteHandler { return &InviteHandler{ CreateInviteUseCase: createInvite, CancelInviteUseCase: cancelInvite, RenewInviteUseCase: renewInvite, ListUsersAndInvitesUseCase: listUsersAndInvites, } } func (h *InviteHandler) CreateInvite(w http.ResponseWriter, r *http.Request) { var request CreateInviteRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest) return } if err := validator.Validate.Struct(request); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } created, err := h.CreateInviteUseCase.CreateInvite(r.Context(), useCases.CreateInviteCommand{ Caller: access.CallerOf(r), Email: request.Email, InvitedBy: userId, RoleID: request.RoleID, }) if err != nil { writeCreateInviteError(w, err) return } writeInviteResponse(w, http.StatusCreated, created) } func writeInviteResponse(w http.ResponseWriter, status int, entry useCases.InviteView) { body, err := json.Marshal(inviteResponse{ ID: entry.ID, Kind: useCases.EntryKindInvite, Email: entry.Email, Status: string(entry.StatusAt(time.Now())), Role: entry.Role, CreatedAt: entry.CreatedAt, ExpiresAt: entry.ExpiresAt, }) if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "no-store") w.WriteHeader(status) _, _ = w.Write(body) } func parseInviteId(r *http.Request) (uint32, error) { id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) return uint32(id), err } func (h *InviteHandler) CancelInvite(w http.ResponseWriter, r *http.Request) { id, err := parseInviteId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } if err := h.CancelInviteUseCase.CancelInvite(r.Context(), id); err != nil { writeManageInviteError(w, err) return } w.WriteHeader(http.StatusNoContent) } func (h *InviteHandler) RenewInvite(w http.ResponseWriter, r *http.Request) { id, err := parseInviteId(r) if err != nil { http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest) return } userId, ok := claims.UserId(r) if !ok { http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) return } renewed, err := h.RenewInviteUseCase.RenewInvite(r.Context(), useCases.RenewInviteCommand{Caller: access.CallerOf(r), ID: id, RenewedBy: userId}) if err != nil { writeManageInviteError(w, err) return } writeInviteResponse(w, http.StatusOK, renewed) } func writeRoleRefusal(w http.ResponseWriter, err error) bool { var escalation role.EscalationError switch { case errors.As(err, &escalation): w.Header().Set(apperr.EscalationHeader, escalation.Key) http.Error(w, apperr.EscalationErrorMessage, http.StatusForbidden) case errors.Is(err, role.ErrRoleLocked): http.Error(w, apperr.RoleLockedErrorMessage, http.StatusForbidden) case errors.Is(err, role.ErrUnknownRole): http.Error(w, apperr.InvalidRoleErrorMessage, http.StatusBadRequest) default: return false } return true } func writeManageInviteError(w http.ResponseWriter, err error) { if writeRoleRefusal(w, err) { return } switch { case errors.Is(err, invite.ErrInviteNotFound): http.Error(w, apperr.InviteNotFoundErrorMessage, http.StatusNotFound) case errors.Is(err, invite.ErrInviteAlreadyAccepted): http.Error(w, apperr.InviteAlreadyAcceptedErrorMessage, http.StatusConflict) case errors.Is(err, invite.ErrInviteEmailNotSent): http.Error(w, apperr.InviteNotSentErrorMessage, http.StatusInternalServerError) case errors.Is(err, user.ErrUserNotFound): http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } } func writeCreateInviteError(w http.ResponseWriter, err error) { if writeRoleRefusal(w, err) { return } switch { case errors.Is(err, invite.ErrEmailHasAccount): http.Error(w, apperr.EmailHasAccountErrorMessage, http.StatusConflict) case errors.Is(err, invite.ErrPendingInvite): http.Error(w, apperr.PendingInviteErrorMessage, http.StatusConflict) case errors.Is(err, invite.ErrInviteEmailNotSent): http.Error(w, apperr.InviteNotSentErrorMessage, http.StatusInternalServerError) case errors.Is(err, user.ErrUserNotFound): http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } } func (h *InviteHandler) ListUsers(w http.ResponseWriter, _ *http.Request) { entries, err := h.ListUsersAndInvitesUseCase.ListUsersAndInvites() if err != nil { http.Error(w, "Internal server error", http.StatusInternalServerError) return } items := make([]any, 0, len(entries)) for _, entry := range entries { if entry.Kind == useCases.EntryKindInvite { items = append(items, inviteListItem{entry.ID, entry.Kind, entry.Email, entry.Role, entry.Status, entry.CreatedAt}) continue } items = append(items, userListItemOf(entry)) } writeNoStoreJSON(w, http.StatusOK, items) }