package api import ( "encoding/json" "errors" "fmt" "net/http" "strings" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" ) type locationInput struct { ParentID *int `json:"parent_id"` ClearParentID bool `json:"clear_parent_id"` Name *string `json:"name"` Description *string `json:"description"` ImageData *string `json:"image_data"` ImageID *int `json:"image_id"` Kind *string `json:"kind"` AreaSQM *float64 `json:"area_sqm"` SunExposure *string `json:"sun_exposure"` SoilCondition *string `json:"soil_condition"` SoilReaction *string `json:"soil_reaction"` Attributes json.RawMessage `json:"attributes"` } func (input locationInput) apply(location *storage.Location) { if input.ParentID != nil { location.ParentID = input.ParentID } if input.ClearParentID { location.ParentID = nil } if input.Name != nil { location.Name = strings.TrimSpace(*input.Name) } if input.Description != nil { location.Description = strings.TrimSpace(*input.Description) } if input.ImageData != nil { location.ImageData = *input.ImageData } if input.Kind != nil { location.Kind = strings.TrimSpace(*input.Kind) } if input.AreaSQM != nil { location.AreaSQM = input.AreaSQM } if input.SunExposure != nil { assignStringPointer(input.SunExposure, &location.SunExposure) } assignStringPointer(input.SoilCondition, &location.SoilCondition) assignStringPointer(input.SoilReaction, &location.SoilReaction) if len(input.Attributes) != 0 { location.Attributes = input.Attributes } } func (app *application) createLocationHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) var input locationInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } user, _ := app.contextGetAuthenticatedUser(r) location := storage.Location{GardenID: gardenID, Attributes: json.RawMessage(`{}`), CreatedBy: user.ID, UpdatedBy: user.ID} input.apply(&location) if !app.validateLocationForGarden(w, r, gardenID, location) { return } imageID, err := app.resolveImage(gardenID, location.ImageData, input.ImageID, user.ID, "location") if err != nil { app.badRequestResponse(w, r, err) return } location.ImageID = imageID location.ImageData = "" location, err = app.models.Locations.Insert(location) if err != nil { app.respondToEntityModelError(w, r, err) return } headers := make(http.Header) headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/locations/%d", gardenID, location.ID)) if err := app.writeJSON(w, http.StatusCreated, envelope{"location": location}, headers); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) listLocationsHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) locations, err := app.models.Locations.GetAllForGarden(gardenID) if err != nil { app.serverErrorResponse(w, r, err) return } user, _ := app.contextGetAuthenticatedUser(r) member, _ := app.contextGetGardenMember(r) visible := locations[:0] for _, location := range locations { if location.CreatedBy == user.ID && member.Can(storage.GardenPermissionLocationReadOwn) || location.CreatedBy != user.ID && member.Can(storage.GardenPermissionLocationReadOther) { visible = append(visible, location) } } if err := app.writeJSON(w, http.StatusOK, envelope{"locations": visible}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) showLocationHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } location, err := app.models.Locations.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationReadOwn, storage.GardenPermissionLocationReadOther) { return } if err := app.writeJSON(w, http.StatusOK, envelope{"location": location}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) updateLocationHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } location, err := app.models.Locations.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationUpdateOwn, storage.GardenPermissionLocationUpdateOther) { return } var input locationInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } previousImageID := location.ImageID previousImageData := location.ImageData input.apply(&location) user, _ := app.contextGetAuthenticatedUser(r) location.UpdatedBy = user.ID if !app.validateLocationForGarden(w, r, gardenID, location) { return } if input.ImageData != nil && *input.ImageData != previousImageData { location.ImageID, err = app.resolveImage(gardenID, *input.ImageData, input.ImageID, user.ID, "location") if err != nil { app.badRequestResponse(w, r, err) return } } location.ImageData = "" location, err = app.models.Locations.Update(gardenID, location) if err != nil { app.respondToEntityModelError(w, r, err) return } if err = app.recordImageAssignment(storage.ImageAssignment{GardenID: gardenID, EntityType: "location", EntityID: location.ID, PreviousImageID: previousImageID, ImageID: location.ImageID, ChangedBy: user.ID}); err != nil { app.serverErrorResponse(w, r, err) return } if err := app.writeJSON(w, http.StatusOK, envelope{"location": location}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deleteLocationHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } location, err := app.models.Locations.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationDeleteOwn, storage.GardenPermissionLocationDeleteOther) { return } if err := app.models.Locations.Delete(gardenID, id); err != nil { app.respondToEntityModelError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) validateLocationForGarden(w http.ResponseWriter, r *http.Request, gardenID int, location storage.Location) bool { v := validate.New() validateImageData(v, location.ImageData) storage.ValidateLocation(v, location) if location.ParentID != nil && *location.ParentID > 0 && *location.ParentID != location.ID { if _, err := app.models.Locations.Get(gardenID, *location.ParentID); err != nil { if errors.Is(err, storage.ErrRecordNotFound) { v.AddError("parent_id", "must refer to a location in this garden") } else { app.serverErrorResponse(w, r, err) return false } } } if v.Valid() && location.ParentID != nil && location.ID > 0 { locations, err := app.models.Locations.GetAllForGarden(gardenID) if err != nil { app.serverErrorResponse(w, r, err) return false } parents := make(map[int]*int, len(locations)) for i := range locations { parents[locations[i].ID] = locations[i].ParentID } seen := map[int]bool{} for current := location.ParentID; current != nil; current = parents[*current] { if *current == location.ID || seen[*current] { v.AddError("parent_id", "must not create a cycle") break } seen[*current] = true } } if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return false } return true }