package httpapi import ( "context" "time" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/store" ) // JSON shapes are camelCase like the backend's responses. type eventJSON struct { ID string `json:"id"` Slug string `json:"slug"` Title string `json:"title"` TitleEs string `json:"titleEs,omitempty"` StartDatetime string `json:"startDatetime"` Status string `json:"status"` } type galleryJSON struct { ID string `json:"id"` Slug string `json:"slug"` Title string `json:"title"` TitleEs string `json:"titleEs,omitempty"` Description string `json:"description,omitempty"` DescriptionEs string `json:"descriptionEs,omitempty"` EventID string `json:"eventId,omitempty"` Visibility string `json:"visibility"` CoverPhotoID string `json:"coverPhotoId,omitempty"` PhotoCount int `json:"photoCount"` CoverURL string `json:"coverUrl,omitempty"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Event *eventJSON `json:"event,omitempty"` // Admin-only fields: ShareToken string `json:"shareToken,omitempty"` ShareURL string `json:"shareUrl,omitempty"` } type photoURLs struct { Thumb string `json:"thumb,omitempty"` Preview string `json:"preview,omitempty"` Original string `json:"original"` } type photoJSON struct { ID string `json:"id"` GalleryID string `json:"galleryId"` Position int `json:"position"` OriginalFilename string `json:"originalFilename,omitempty"` ContentType string `json:"contentType"` SizeBytes int64 `json:"sizeBytes"` Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` TakenAt string `json:"takenAt,omitempty"` Status string `json:"status"` LastError string `json:"lastError,omitempty"` // admin only CreatedAt string `json:"createdAt"` URLs photoURLs `json:"urls"` } // viewTokenFor returns the token to embed in a gallery's file URLs: none // for public galleries (files are anonymously accessible), a short-lived // gallery-scoped view token otherwise (see viewtoken.go). func (s *Server) viewTokenFor(g store.Gallery) string { if g.Visibility == store.VisibilityPublic { return "" } return mintViewToken([]byte(s.cfg.ViewTokenSecret), g.ID) } func isoTime(t time.Time) string { if t.IsZero() { return "" } return t.UTC().Format("2006-01-02T15:04:05.000Z") } // fileURL builds the access-checked file endpoint URL; token is appended // for link-mode viewers so the client can use URLs verbatim. func fileURL(photoID, variant, token string) string { u := "/api/photos/files/" + photoID + "/" + variant if token != "" { u += "?token=" + token } return u } func (s *Server) photoToJSON(p store.Photo, token string, admin bool) photoJSON { out := photoJSON{ ID: p.ID, GalleryID: p.GalleryID, Position: p.Position, OriginalFilename: p.OriginalFilename, ContentType: p.ContentType, SizeBytes: p.SizeBytes, Width: p.Width, Height: p.Height, TakenAt: isoTime(p.TakenAt), Status: p.Status, CreatedAt: isoTime(p.CreatedAt), URLs: photoURLs{Original: fileURL(p.ID, "original", token)}, } if p.ThumbKey != "" { out.URLs.Thumb = fileURL(p.ID, "thumb", token) } if p.PreviewKey != "" { out.URLs.Preview = fileURL(p.ID, "preview", token) } if admin { out.LastError = p.LastError } return out } func (s *Server) galleryToJSON(ctx context.Context, g store.Gallery, token string, admin bool) galleryJSON { out := galleryJSON{ ID: g.ID, Slug: g.Slug, Title: g.Title, TitleEs: g.TitleEs, Description: g.Description, DescriptionEs: g.DescriptionEs, EventID: g.EventID, Visibility: g.Visibility, CoverPhotoID: g.CoverPhotoID, PhotoCount: g.PhotoCount, CreatedAt: isoTime(g.CreatedAt), UpdatedAt: isoTime(g.UpdatedAt), } if coverID, _ := s.db.GalleryCoverKey(ctx, g); coverID != "" { out.CoverURL = fileURL(coverID, "thumb", token) } if g.EventID != "" { if ev, err := s.db.GetEventSummary(ctx, g.EventID); err == nil { out.Event = &eventJSON{ ID: ev.ID, Slug: ev.Slug, Title: ev.Title, TitleEs: ev.TitleEs, StartDatetime: isoTime(ev.StartDatetime), Status: ev.Status, } } } if admin { out.ShareToken = g.ShareToken // Event-linked galleries live under the event's URL; standalone // galleries keep their own /photos/ URL. page := "/photos/" + g.Slug if out.Event != nil { page = "/events/" + out.Event.Slug + "/gallery" } out.ShareURL = s.cfg.FrontendURL + page // The token only grants anything in link/ticket mode; public and // private galleries get a clean URL. if g.Visibility == store.VisibilityLink || g.Visibility == store.VisibilityTicket { out.ShareURL += "?token=" + g.ShareToken } } return out }