package api import ( "log/slog" "net/http" "time" "github.com/alchemistkay/guestguard/internal/auth" "github.com/alchemistkay/guestguard/internal/storage" ) type Server struct { logger *slog.Logger db *storage.DB hub *Hub users *userHandler events *eventHandler guests *guestHandler tokens *tokenHandler rsvps *rsvpHandler activity *activityHandler ws *wsHandler health *healthHandler } type ServerDeps struct { Logger *slog.Logger DB *storage.DB Hub *Hub AccessPublisher accessPublisher RSVPPublisher rsvpPublisher FraudScorer fraudScorer TokenTTL time.Duration } func NewServer(deps ServerDeps) *Server { eventRepo := storage.NewEventRepo(deps.DB) guestRepo := storage.NewGuestRepo(deps.DB) tokenRepo := storage.NewTokenRepo(deps.DB) rsvpRepo := storage.NewRSVPRepo(deps.DB) accessRepo := storage.NewAccessLogRepo(deps.DB) userRepo := storage.NewUserRepo(deps.DB) hub := deps.Hub if hub == nil { hub = NewHub(deps.Logger) } return &Server{ logger: deps.Logger, db: deps.DB, hub: hub, users: &userHandler{repo: userRepo}, events: &eventHandler{repo: eventRepo}, guests: &guestHandler{guests: guestRepo, events: eventRepo}, tokens: &tokenHandler{ logger: deps.Logger, guests: guestRepo, tokens: tokenRepo, events: eventRepo, accessLogs: accessRepo, gen: auth.NewGenerator(), ttl: deps.TokenTTL, pub: deps.AccessPublisher, }, rsvps: &rsvpHandler{ logger: deps.Logger, guests: guestRepo, tokens: tokenRepo, events: eventRepo, rsvps: rsvpRepo, accessLogs: accessRepo, scorer: deps.FraudScorer, pub: deps.RSVPPublisher, }, activity: &activityHandler{ events: eventRepo, rsvps: rsvpRepo, accessLogs: accessRepo, }, ws: &wsHandler{logger: deps.Logger, hub: hub}, health: &healthHandler{pool: deps.DB.Pool}, } } func (s *Server) Hub() *Hub { return s.hub } func (s *Server) Handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.health.live) mux.HandleFunc("GET /health/ready", s.health.ready) mux.HandleFunc("POST /users", s.users.upsert) mux.HandleFunc("POST /events", s.events.create) mux.HandleFunc("GET /events", s.events.list) mux.HandleFunc("GET /events/{id}", s.events.get) mux.HandleFunc("PATCH /events/{id}", s.events.update) mux.HandleFunc("DELETE /events/{id}", s.events.delete) mux.HandleFunc("POST /events/{id}/guests", s.guests.create) mux.HandleFunc("GET /events/{id}/guests", s.guests.list) mux.HandleFunc("GET /events/{id}/activity", s.activity.list) mux.HandleFunc("POST /events/{id}/guests/{guest_id}/tokens", s.tokens.issue) mux.HandleFunc("GET /access/{token}", s.tokens.access) mux.HandleFunc("POST /rsvp/{token}", s.rsvps.submit) mux.HandleFunc("GET /ws/events/{id}", s.ws.handle) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotFound, "not found") }) var h http.Handler = mux h = corsMiddleware(h) h = loggingMiddleware(s.logger)(h) h = recoverMiddleware(s.logger)(h) return h } // Permissive CORS for the dev frontend on a different origin. In production // the frontend is served from the same domain so this is largely a no-op. func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Device-Fingerprint") } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }