package httpapi import ( "log" "net/http" "strconv" "time" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/auth" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/store" ) func validVisibility(v string) bool { switch v { case store.VisibilityPublic, store.VisibilityPrivate, store.VisibilityLink, store.VisibilityTicket: return true } return false } type createGalleryBody struct { Title string `json:"title"` TitleEs string `json:"titleEs"` Description string `json:"description"` DescriptionEs string `json:"descriptionEs"` EventID string `json:"eventId"` Visibility string `json:"visibility"` } func (s *Server) createGallery(w http.ResponseWriter, r *http.Request, user auth.User) { var body createGalleryBody if err := decodeJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON body") return } if body.Title == "" { writeError(w, http.StatusBadRequest, "title is required") return } if body.Visibility == "" { body.Visibility = store.VisibilityPrivate } if !validVisibility(body.Visibility) { writeError(w, http.StatusBadRequest, "visibility must be public, private, link or ticket") return } if body.EventID != "" { if _, err := s.db.GetEventSummary(r.Context(), body.EventID); err != nil { writeStoreError(w, err, "Event not found") return } } slug, err := s.uniqueSlug(r.Context(), body.Title) if err != nil { writeStoreError(w, err, "") return } now := time.Now() g := store.Gallery{ ID: newID(), Slug: slug, Title: body.Title, TitleEs: body.TitleEs, Description: body.Description, DescriptionEs: body.DescriptionEs, EventID: body.EventID, Visibility: body.Visibility, ShareToken: newShareToken(), CreatedBy: user.ID, CreatedAt: now, UpdatedAt: now, } if err := s.db.CreateGallery(r.Context(), g); err != nil { writeStoreError(w, err, "") return } writeJSON(w, http.StatusCreated, map[string]any{ "gallery": s.galleryToJSON(r.Context(), g, s.viewTokenFor(g), true), }) } func (s *Server) listGalleries(w http.ResponseWriter, r *http.Request, _ auth.User) { limit := queryInt(r, "limit", 100) offset := queryInt(r, "offset", 0) galleries, err := s.db.ListGalleries(r.Context(), r.URL.Query().Get("eventId"), limit, offset) if err != nil { writeStoreError(w, err, "") return } out := make([]galleryJSON, 0, len(galleries)) for _, g := range galleries { out = append(out, s.galleryToJSON(r.Context(), g, s.viewTokenFor(g), true)) } writeJSON(w, http.StatusOK, map[string]any{"galleries": out}) } func (s *Server) getGallery(w http.ResponseWriter, r *http.Request, _ auth.User) { g, err := s.db.GetGallery(r.Context(), r.PathValue("id")) if err != nil { writeStoreError(w, err, "Gallery not found") return } photos, err := s.db.ListPhotos(r.Context(), g.ID, false) if err != nil { writeStoreError(w, err, "") return } urlToken := s.viewTokenFor(g) out := make([]photoJSON, 0, len(photos)) for _, p := range photos { out = append(out, s.photoToJSON(p, urlToken, true)) } writeJSON(w, http.StatusOK, map[string]any{ "gallery": s.galleryToJSON(r.Context(), g, urlToken, true), "photos": out, }) } type updateGalleryBody struct { Title *string `json:"title"` TitleEs *string `json:"titleEs"` Description *string `json:"description"` DescriptionEs *string `json:"descriptionEs"` EventID *string `json:"eventId"` Visibility *string `json:"visibility"` CoverPhotoID *string `json:"coverPhotoId"` } func (s *Server) updateGallery(w http.ResponseWriter, r *http.Request, _ auth.User) { id := r.PathValue("id") var body updateGalleryBody if err := decodeJSON(r, &body); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON body") return } if body.Title != nil && *body.Title == "" { writeError(w, http.StatusBadRequest, "title cannot be empty") return } if body.Visibility != nil && !validVisibility(*body.Visibility) { writeError(w, http.StatusBadRequest, "visibility must be public, private, link or ticket") return } if body.EventID != nil && *body.EventID != "" { if _, err := s.db.GetEventSummary(r.Context(), *body.EventID); err != nil { writeStoreError(w, err, "Event not found") return } } if body.CoverPhotoID != nil && *body.CoverPhotoID != "" { p, err := s.db.GetPhoto(r.Context(), *body.CoverPhotoID) if err != nil || p.GalleryID != id { writeError(w, http.StatusBadRequest, "coverPhotoId must be a photo of this gallery") return } } err := s.db.UpdateGallery(r.Context(), id, store.GalleryUpdate{ Title: body.Title, TitleEs: body.TitleEs, Description: body.Description, DescriptionEs: body.DescriptionEs, EventID: body.EventID, Visibility: body.Visibility, CoverPhotoID: body.CoverPhotoID, }) if err != nil { writeStoreError(w, err, "Gallery not found") return } g, err := s.db.GetGallery(r.Context(), id) if err != nil { writeStoreError(w, err, "Gallery not found") return } writeJSON(w, http.StatusOK, map[string]any{ "gallery": s.galleryToJSON(r.Context(), g, s.viewTokenFor(g), true), }) } func (s *Server) deleteGallery(w http.ResponseWriter, r *http.Request, _ auth.User) { id := r.PathValue("id") keys, err := s.db.PhotoKeys(r.Context(), id) if err != nil { writeStoreError(w, err, "") return } if err := s.db.DeleteGallery(r.Context(), id); err != nil { writeStoreError(w, err, "Gallery not found") return } // Object cleanup is best-effort after the rows are gone; orphaned // objects are harmless (unreachable) and logged for manual sweep. for _, key := range keys { if err := s.storage.Delete(r.Context(), key); err != nil { log.Printf("delete object %s: %v", key, err) } } writeJSON(w, http.StatusOK, map[string]string{"message": "Gallery deleted"}) } func (s *Server) rotateShareToken(w http.ResponseWriter, r *http.Request, _ auth.User) { id := r.PathValue("id") if err := s.db.RotateShareToken(r.Context(), id, newShareToken()); err != nil { writeStoreError(w, err, "Gallery not found") return } g, err := s.db.GetGallery(r.Context(), id) if err != nil { writeStoreError(w, err, "Gallery not found") return } writeJSON(w, http.StatusOK, map[string]any{ "gallery": s.galleryToJSON(r.Context(), g, s.viewTokenFor(g), true), }) } func queryInt(r *http.Request, key string, def int) int { if v := r.URL.Query().Get(key); v != "" { if n, err := strconv.Atoi(v); err == nil && n >= 0 { return n } } return def }