package api import ( "encoding/json" "errors" "fmt" "net/http" "strings" "time" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" ) type plantInput struct { Tags []string `json:"tags"` SpeciesID *int `json:"species_id"` ClearSpeciesID bool `json:"clear_species_id"` Name *string `json:"name"` Notes *string `json:"notes"` ImageData *string `json:"image_data"` ImageID *int `json:"image_id"` AcquiredAt *time.Time `json:"acquired_at"` ClearAcquiredAt bool `json:"clear_acquired_at"` Status *string `json:"status"` RemovedAt *time.Time `json:"removed_at"` Attributes json.RawMessage `json:"attributes"` } func (input plantInput) apply(plant *storage.Plant) { if input.SpeciesID != nil { plant.SpeciesID = input.SpeciesID } if input.ClearSpeciesID { plant.SpeciesID = nil } if input.Name != nil { plant.Name = strings.TrimSpace(*input.Name) } if input.Notes != nil { plant.Notes = strings.TrimSpace(*input.Notes) } if input.ImageData != nil { plant.ImageData = *input.ImageData } if input.AcquiredAt != nil { plant.AcquiredAt = input.AcquiredAt } if input.ClearAcquiredAt { plant.AcquiredAt = nil } if input.Status != nil { plant.Status = *input.Status } if input.RemovedAt != nil { plant.RemovedAt = input.RemovedAt } if len(input.Attributes) != 0 { plant.Attributes = input.Attributes } } func (app *application) createPlantHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) var input plantInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } user, _ := app.contextGetAuthenticatedUser(r) plant := storage.Plant{GardenID: gardenID, Status: "alive", Attributes: json.RawMessage(`{}`), CreatedBy: user.ID, UpdatedBy: user.ID, PlantedBy: user.ID} input.apply(&plant) plant.Tags = storage.NormalizeTags(input.Tags) if !app.validatePlantForGarden(w, r, gardenID, plant) { return } var err error plant.ImageID, err = app.resolveImage(gardenID, plant.ImageData, input.ImageID, user.ID, "plant") if err != nil { app.badRequestResponse(w, r, err) return } plant.ImageData = "" plant, err = app.models.Plants.Insert(plant) if err != nil { app.respondToEntityModelError(w, r, err) return } plant.Tags, err = app.saveTags(gardenID, storage.TagEntityPlant, plant.ID, plant.Tags) if err != nil { app.serverErrorResponse(w, r, err) return } headers := make(http.Header) headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/plants/%d", gardenID, plant.ID)) if err := app.writeJSON(w, http.StatusCreated, envelope{"plant": plant}, headers); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) listPlantsHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) plants, err := app.models.Plants.GetAllForGarden(gardenID) if err != nil { app.serverErrorResponse(w, r, err) return } user, _ := app.contextGetAuthenticatedUser(r) member, _ := app.contextGetGardenMember(r) visible := plants[:0] for i := range plants { if plants[i].CreatedBy != user.ID && !member.Can(storage.GardenPermissionPlantReadOther) { continue } if plants[i].CreatedBy == user.ID && !member.Can(storage.GardenPermissionPlantReadOwn) { continue } plants[i].Tags, err = app.loadTags(gardenID, storage.TagEntityPlant, plants[i].ID) if err != nil { app.serverErrorResponse(w, r, err) return } visible = append(visible, plants[i]) } plants = visible if err := app.writeJSON(w, http.StatusOK, envelope{"plants": plants}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) showPlantHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } plant, err := app.models.Plants.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, plant.CreatedBy, storage.GardenPermissionPlantReadOwn, storage.GardenPermissionPlantReadOther) { return } plant.Tags, err = app.loadTags(gardenID, storage.TagEntityPlant, plant.ID) if err != nil { app.serverErrorResponse(w, r, err) return } if err := app.writeJSON(w, http.StatusOK, envelope{"plant": plant}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) updatePlantHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } plant, err := app.models.Plants.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, plant.CreatedBy, storage.GardenPermissionPlantUpdateOwn, storage.GardenPermissionPlantUpdateOther) { return } var input plantInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } previousImageID := plant.ImageID previousImageData := plant.ImageData input.apply(&plant) user, _ := app.contextGetAuthenticatedUser(r) plant.UpdatedBy = user.ID if input.Tags != nil { plant.Tags = storage.NormalizeTags(input.Tags) } if !app.validatePlantForGarden(w, r, gardenID, plant) { return } if input.ImageData != nil && *input.ImageData != previousImageData { plant.ImageID, err = app.resolveImage(gardenID, *input.ImageData, input.ImageID, user.ID, "plant") if err != nil { app.badRequestResponse(w, r, err) return } } plant.ImageData = "" plant, err = app.models.Plants.Update(gardenID, plant) if err != nil { app.respondToEntityModelError(w, r, err) return } if err = app.recordImageAssignment(storage.ImageAssignment{GardenID: gardenID, EntityType: "plant", EntityID: plant.ID, PreviousImageID: previousImageID, ImageID: plant.ImageID, ChangedBy: user.ID}); err != nil { app.serverErrorResponse(w, r, err) return } if input.Tags != nil { plant.Tags, err = app.saveTags(gardenID, storage.TagEntityPlant, plant.ID, plant.Tags) if err != nil { app.serverErrorResponse(w, r, err) return } } if err := app.writeJSON(w, http.StatusOK, envelope{"plant": plant}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deletePlantHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) id, err := app.readIDParam(r) if err != nil { app.notFoundResponse(w, r) return } plant, err := app.models.Plants.Get(gardenID, id) if err != nil { app.respondToEntityModelError(w, r, err) return } if !app.authorizeGardenResource(w, r, plant.CreatedBy, storage.GardenPermissionPlantDeleteOwn, storage.GardenPermissionPlantDeleteOther) { return } if err := app.models.Plants.Delete(gardenID, id); err != nil { app.respondToEntityModelError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) validatePlantForGarden(w http.ResponseWriter, r *http.Request, gardenID int, plant storage.Plant) bool { v := validate.New() validateImageData(v, plant.ImageData) storage.ValidatePlant(v, plant) if plant.SpeciesID != nil && *plant.SpeciesID > 0 { if _, err := app.models.Species.Get(gardenID, *plant.SpeciesID); err != nil { if errors.Is(err, storage.ErrRecordNotFound) { v.AddError("species_id", "must refer to an available species") } else { app.serverErrorResponse(w, r, err) return false } } } if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return false } return true }