197 lines
6.0 KiB
Go
197 lines
6.0 KiB
Go
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)
|
|
}
|
|
}
|