feat: invite management API: DELETE /v1/user/invite/:id (204, cancels a pending invite) and POST /v1/user/invite/:id/renew (200, new code, expires_at = now+3d, created_at = now, resends the invitation email, old link dead)
This commit is contained in:
1 parent
461e93e3b5
commit
153fd8719a
18 files changed
+1147
-44
No files matched your search
@@ -38,6 +38,8 @@ type UseCases struct {
|
||||
UpdateUserProfileUseCase *useCases.UpdateUserProfileUseCase
|
||||
ListUsersUseCase *useCases.ListUsersUseCase
|
||||
CreateInviteUseCase *useCases.CreateInviteUseCase
|
||||
CancelInviteUseCase *useCases.CancelInviteUseCase
|
||||
RenewInviteUseCase *useCases.RenewInviteUseCase
|
||||
ListUsersAndInvitesUseCase *useCases.ListUsersAndInvitesUseCase
|
||||
GetInviteUseCase *useCases.GetInviteUseCase
|
||||
AcceptInviteUseCase *useCases.AcceptInviteUseCase
|
||||
@@ -111,6 +113,8 @@ func getUseCases(repos *Repositories, settings config.Settings) *UseCases {
|
||||
UpdateUserProfileUseCase: useCases.NewUpdateUserProfileUseCase(repos.UserRepo),
|
||||
ListUsersUseCase: useCases.NewListUsersUseCase(repos.UserRepo),
|
||||
CreateInviteUseCase: useCases.NewCreateInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher, repos.MailRepo, settings.PublicURL),
|
||||
CancelInviteUseCase: useCases.NewCancelInviteUseCase(repos.InviteRepo),
|
||||
RenewInviteUseCase: useCases.NewRenewInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher, repos.MailRepo, settings.PublicURL),
|
||||
ListUsersAndInvitesUseCase: useCases.NewListUsersAndInvitesUseCase(repos.UserRepo, repos.InviteRepo),
|
||||
GetInviteUseCase: useCases.NewGetInviteUseCase(repos.InviteRepo, hasher),
|
||||
AcceptInviteUseCase: useCases.NewAcceptInviteUseCase(repos.UserRepo, repos.InviteRepo, hasher),
|
||||
|
||||
@@ -10,11 +10,16 @@ import (
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -50,9 +55,11 @@ type userListItem struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func NewInviteHandler(createInvite *useCases.CreateInviteUseCase, listUsersAndInvites *useCases.ListUsersAndInvitesUseCase) *InviteHandler {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -84,23 +91,82 @@ func (h *InviteHandler) CreateInvite(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
writeInviteResponse(w, http.StatusCreated, created)
|
||||
}
|
||||
|
||||
func writeInviteResponse(w http.ResponseWriter, status int, entry invite.Invite) {
|
||||
body, err := json.Marshal(inviteResponse{
|
||||
ID: created.ID,
|
||||
ID: entry.ID,
|
||||
Kind: useCases.EntryKindInvite,
|
||||
Email: created.Email,
|
||||
Status: string(created.StatusAt(time.Now())),
|
||||
CreatedAt: created.CreatedAt,
|
||||
ExpiresAt: created.ExpiresAt,
|
||||
Email: entry.Email,
|
||||
Status: string(entry.StatusAt(time.Now())),
|
||||
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(http.StatusCreated)
|
||||
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(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(useCases.RenewInviteCommand{ID: id, RenewedBy: userId})
|
||||
if err != nil {
|
||||
writeManageInviteError(w, err)
|
||||
return
|
||||
}
|
||||
writeInviteResponse(w, http.StatusOK, renewed)
|
||||
}
|
||||
|
||||
func writeManageInviteError(w http.ResponseWriter, err error) {
|
||||
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) {
|
||||
switch {
|
||||
case errors.Is(err, invite.ErrEmailHasAccount):
|
||||
|
||||
@@ -19,6 +19,8 @@ const TooManyRequestsErrorMessage = "Too many requests. Please try again later."
|
||||
const EmailHasAccountErrorMessage = "This address already has an account."
|
||||
const PendingInviteErrorMessage = "There's already a pending invite for this address."
|
||||
const InviteNotSentErrorMessage = "Couldn't send the invite. Try again in a moment."
|
||||
const InviteNotFoundErrorMessage = "Invite not found"
|
||||
const InviteAlreadyAcceptedErrorMessage = "This invite has already been accepted."
|
||||
const InviteInvalidErrorMessage = "This invite link doesn't work."
|
||||
const InviteExpiredErrorMessage = "This invite has expired."
|
||||
const OwnerRequiredErrorMessage = "Only the owner can do this."
|
||||
|
||||
+3
-1
@@ -100,7 +100,7 @@ func NewUserRouter(container *UseCases, rateLimit config.AuthRateLimitSettings,
|
||||
r := chi.NewRouter()
|
||||
|
||||
userHandler := user.NewHandler(container.UpdateUserProfileUseCase)
|
||||
inviteHandler := user.NewInviteHandler(container.CreateInviteUseCase, container.ListUsersAndInvitesUseCase)
|
||||
inviteHandler := user.NewInviteHandler(container.CreateInviteUseCase, container.CancelInviteUseCase, container.RenewInviteUseCase, container.ListUsersAndInvitesUseCase)
|
||||
|
||||
r.Use(jwtauth.Verifier(container.Token))
|
||||
r.Use(jwtauth.Authenticator(container.Token))
|
||||
@@ -113,6 +113,8 @@ func NewUserRouter(container *UseCases, rateLimit config.AuthRateLimitSettings,
|
||||
r.Use(middleware.TrustedProxyRealIP(trustedProxies))
|
||||
r.Use(middleware.NewRateLimiter(rateLimit.MaxAttempts, rateLimit.Window))
|
||||
r.Post("/invite", inviteHandler.CreateInvite)
|
||||
r.Delete("/invite/{id}", inviteHandler.CancelInvite)
|
||||
r.Post("/invite/{id}/renew", inviteHandler.RenewInvite)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user