Introduces the Go photo-api service, nginx/systemd deploy wiring, and Next.js gallery/lightbox pages so event photos can be managed and browsed. Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
3.6 KiB
Go
91 lines
3.6 KiB
Go
// 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)
|
|
}
|