// Package httpapi exposes the photo-api HTTP surface under /api/photos, // following the backend's route and response conventions (resource-keyed // success bodies, {"error": string} failures, Bearer auth). package httpapi import ( "log" "net/http" "time" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/auth" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/config" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/storage" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/store" "git.azzamo.net/Michilis/Spanglish/photo-api/internal/worker" ) type Server struct { cfg config.Config db *store.DB storage storage.Storage verifier *auth.Verifier worker *worker.Worker } func New(cfg config.Config, db *store.DB, st storage.Storage, verifier *auth.Verifier, w *worker.Worker) *Server { return &Server{cfg: cfg, db: db, storage: st, verifier: verifier, worker: w} } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() health := func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } mux.HandleFunc("GET /health", health) mux.HandleFunc("GET /api/photos/health", health) // Admin surface: role admin|organizer only (mirrors /api/media). mux.HandleFunc("POST /api/photos/galleries", s.requireAdmin(s.createGallery)) mux.HandleFunc("GET /api/photos/galleries", s.requireAdmin(s.listGalleries)) mux.HandleFunc("GET /api/photos/galleries/{id}", s.requireAdmin(s.getGallery)) mux.HandleFunc("PATCH /api/photos/galleries/{id}", s.requireAdmin(s.updateGallery)) mux.HandleFunc("DELETE /api/photos/galleries/{id}", s.requireAdmin(s.deleteGallery)) mux.HandleFunc("POST /api/photos/galleries/{id}/photos", s.requireAdmin(s.uploadPhotos)) mux.HandleFunc("PATCH /api/photos/galleries/{id}/order", s.requireAdmin(s.reorderPhotos)) mux.HandleFunc("POST /api/photos/galleries/{id}/share-token", s.requireAdmin(s.rotateShareToken)) mux.HandleFunc("DELETE /api/photos/photos/{photoId}", s.requireAdmin(s.deletePhoto)) mux.HandleFunc("POST /api/photos/photos/{photoId}/retry", s.requireAdmin(s.retryPhoto)) // Viewer surface: optional auth, checked per gallery visibility. mux.HandleFunc("GET /api/photos/public/galleries", s.listPublicGalleries) mux.HandleFunc("GET /api/photos/public/galleries/{slug}", s.getPublicGallery) mux.HandleFunc("GET /api/photos/public/events/{eventSlug}/gallery", s.getEventGallery) mux.HandleFunc("GET /api/photos/files/{photoId}/{variant}", s.serveFile) return s.withCommon(mux) } // withCommon adds request logging and a same-spirit CORS allowance as the // backend's cors() (origin = FRONTEND_URL); in production nginx serves // same-origin so this mostly matters in development. func (s *Server) withCommon(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if origin := r.Header.Get("Origin"); origin != "" && origin == s.cfg.FrontendURL { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") w.Header().Set("Vary", "Origin") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } start := time.Now() sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r) log.Printf("%s %s %d %s", r.Method, r.URL.Path, sw.status, time.Since(start).Round(time.Millisecond)) }) } type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) }