Files
Spanglish/photo-api/internal/httpapi/public.go
T
MichilisandClaude Opus 4.6 617c884012 Add view tokens for non-public gallery images and per-mode gate pages.
<img> tags cannot send Authorization headers, so non-public gallery photos
were invisible even to authorized viewers. The server now mints short-lived
HMAC view tokens (gallery-scoped, hour-bucketed) and embeds them in every
file URL for non-public galleries. Access denials return distinct 403
messages per visibility mode, and the frontend renders a matching gate page
(private, link-only, ticket-holders, login prompt) with an inline login
modal so visitors never leave the gallery page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-26 06:48:05 +00:00

78 lines
2.4 KiB
Go

package httpapi
import (
"net/http"
"git.azzamo.net/Michilis/Spanglish/photo-api/internal/store"
)
// listPublicGalleries is the public index: only visibility='public'
// galleries ever appear here.
func (s *Server) listPublicGalleries(w http.ResponseWriter, r *http.Request) {
galleries, err := s.db.ListPublicGalleries(r.Context())
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, "", false))
}
writeJSON(w, http.StatusOK, map[string]any{"galleries": out})
}
// getPublicGallery serves a single gallery to any authorized viewer.
// ?token= carries the share token for link/ticket modes.
func (s *Server) getPublicGallery(w http.ResponseWriter, r *http.Request) {
g, err := s.db.GetGalleryBySlug(r.Context(), r.PathValue("slug"))
if err != nil {
writeStoreError(w, err, "Gallery not found")
return
}
s.respondGalleryView(w, r, g)
}
// getEventGallery serves the newest gallery linked to an event, backing the
// /events/{slug}/gallery public page. Same access rules as by-slug.
func (s *Server) getEventGallery(w http.ResponseWriter, r *http.Request) {
ev, err := s.db.GetEventSummaryBySlug(r.Context(), r.PathValue("eventSlug"))
if err != nil {
writeStoreError(w, err, "Gallery not found")
return
}
g, err := s.db.GetGalleryByEventID(r.Context(), ev.ID)
if err != nil {
writeStoreError(w, err, "Gallery not found")
return
}
s.respondGalleryView(w, r, g)
}
func (s *Server) respondGalleryView(w http.ResponseWriter, r *http.Request, g store.Gallery) {
user := s.optionalUser(r)
token := r.URL.Query().Get("token")
if denial := s.authorize(r, g, user, token); denial != nil {
writeError(w, denial.status, denial.msg)
return
}
// Non-public galleries get a short-lived view token on every file URL,
// because the browser's <img> requests cannot carry the Bearer header
// that authorized this gallery fetch. Public galleries need none.
urlToken := s.viewTokenFor(g)
photos, err := s.db.ListPhotos(r.Context(), g.ID, true)
if err != nil {
writeStoreError(w, err, "")
return
}
out := make([]photoJSON, 0, len(photos))
for _, p := range photos {
out = append(out, s.photoToJSON(p, urlToken, false))
}
writeJSON(w, http.StatusOK, map[string]any{
"gallery": s.galleryToJSON(r.Context(), g, urlToken, false),
"photos": out,
})
}