package api import ( "errors" "net/http" "strings" "time" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" "github.com/julienschmidt/httprouter" ) func (app *application) listGardenMembersHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) members, err := app.models.GardenMembers.GetAllForGarden(gardenID) if err != nil { app.serverErrorResponse(w, r, err) return } if err = app.writeJSON(w, http.StatusOK, envelope{"members": members}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func canManageMember(target, replacement storage.GardenRole) bool { return target != storage.GardenRoleOwner && replacement != storage.GardenRoleOwner } func (app *application) updateGardenMemberHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) userID, err := app.readNamedIDParam(r, "userID") if err != nil { app.notFoundResponse(w, r) return } var input struct { Role storage.GardenRole `json:"role"` } if err = app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } _, roleErr := app.models.Roles.GetForGarden(gardenID, string(input.Role)) if roleErr != nil || input.Role == storage.GardenRoleOwner { app.failedValidationResponse(w, r, map[string]string{"role": "must be a garden role; ownership must be transferred separately"}) return } target, err := app.models.GardenMembers.Get(gardenID, userID) if err != nil { app.respondToMemberError(w, r, err) return } if !canManageMember(target.Role, input.Role) { app.permissionDeniedResponse(w, r) return } target.Role = input.Role target, err = app.models.GardenMembers.Update(target) if err != nil { app.respondToMemberError(w, r, err) return } if err = app.writeJSON(w, http.StatusOK, envelope{"member": target}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deleteGardenMemberHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) userID, err := app.readNamedIDParam(r, "userID") if err != nil { app.notFoundResponse(w, r) return } target, err := app.models.GardenMembers.Get(gardenID, userID) if err != nil { app.respondToMemberError(w, r, err) return } if !canManageMember(target.Role, storage.GardenRoleViewer) { app.permissionDeniedResponse(w, r) return } if err = app.models.GardenMembers.Delete(gardenID, userID); err != nil { app.respondToMemberError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) transferGardenOwnershipHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) toUserID, err := app.readNamedIDParam(r, "userID") if err != nil { app.notFoundResponse(w, r) return } actor, _ := app.contextGetGardenMember(r) if err = app.models.GardenMembers.TransferOwnership(gardenID, actor.UserID, toUserID); err != nil { app.respondToMemberError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) listGardenInvitesHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) invites, err := app.models.GardenInvites.GetAllForGarden(gardenID) if err != nil { app.serverErrorResponse(w, r, err) return } if err = app.writeJSON(w, http.StatusOK, envelope{"invites": invites}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) createGardenInviteHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) actor, _ := app.contextGetGardenMember(r) var input struct { Email string `json:"email"` Role storage.GardenRole `json:"role"` } if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } input.Email = strings.ToLower(strings.TrimSpace(input.Email)) v := validate.New() storage.ValidateEmail(v, input.Email) _, roleErr := app.models.Roles.GetForGarden(gardenID, string(input.Role)) if roleErr != nil || input.Role == storage.GardenRoleOwner { v.AddError("role", "role may not be granted") } if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return } invite, err := app.models.GardenInvites.Upsert(storage.GardenInvite{GardenID: gardenID, Email: input.Email, Role: input.Role, InvitedBy: actor.UserID, ExpiresAt: time.Now().Add(7 * 24 * time.Hour)}) if err != nil { app.serverErrorResponse(w, r, err) return } inviteEmail := invite.Email inviteURL := strings.TrimRight(app.config.WebBaseURL, "/") + "/invite?token=" + invite.Token app.background(func() { data := map[string]any{"inviteURL": inviteURL} if sendErr := app.mailer.Send(inviteEmail, "garden_invite.tmpl", data); sendErr != nil { app.logger.Error(sendErr.Error()) } }) invite.Token = "" if err = app.writeJSON(w, http.StatusCreated, envelope{"invite": invite}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deleteGardenInviteHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) inviteID, err := app.readNamedIDParam(r, "inviteID") if err != nil { app.notFoundResponse(w, r) return } if err = app.models.GardenInvites.Delete(gardenID, inviteID); err != nil { app.respondToMemberError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) acceptGardenInviteHandler(w http.ResponseWriter, r *http.Request) { token := httprouter.ParamsFromContext(r.Context()).ByName("token") user, _ := app.contextGetAuthenticatedUser(r) member, err := app.models.GardenInvites.Accept(token, user) if err != nil { app.respondToMemberError(w, r, err) return } if err = app.writeJSON(w, http.StatusOK, envelope{"member": member}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) respondToMemberError(w http.ResponseWriter, r *http.Request, err error) { switch { case errors.Is(err, storage.ErrRecordNotFound): app.notFoundResponse(w, r) case errors.Is(err, storage.ErrConflict): app.conflictResponse(w, r) default: app.serverErrorResponse(w, r, err) } }