package user import ( "RenewCMS/api/pkg/access" "RenewCMS/api/pkg/apperr" "RenewCMS/api/pkg/claims" "RenewCMS/api/pkg/payload" validator "RenewCMS/api/pkg/validator" "RenewCMS/internal/domain/invite" "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 payload.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 *payload.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 *payload.RoleRef `json:"role"` RoleAssignedBy *payload.RoleAssigner `json:"role_assigned_by"` 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: payload.RoleRefOf(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 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): w.Header().Set(apperr.ErrorCodeHeader, apperr.EmailHasAccountErrorCode) http.Error(w, apperr.EmailHasAccountErrorMessage, http.StatusConflict) case errors.Is(err, invite.ErrPendingInvite): w.Header().Set(apperr.ErrorCodeHeader, apperr.InvitePendingErrorCode) 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{ ID: entry.ID, Kind: entry.Kind, Email: entry.Email, Role: payload.RoleRefPtrOf(entry.Role), Status: entry.Status, CreatedAt: entry.CreatedAt, }) continue } items = append(items, userListItemOf(entry)) } writeNoStoreJSON(w, http.StatusOK, items) }