3f8bc58ca9
Phase 1 — Core API (Go): - Events, guests, tokens, RSVPs CRUD on PostgreSQL via pgx/v5 - HMAC-signed per-guest tokens with format validation - Health endpoint with DB ping, slog JSON logging, graceful shutdown Phase 2 — NATS + Fraud Engine: - NATS JetStream pub/sub with explicit-ack consumers - Python/FastAPI fraud engine with heuristic risk scoring (fingerprint mismatch, IP change, missing signals, repeated access) - gRPC sync scoring with 250ms fail-open timeout - Per-guest baseline tracking; risk bands low/medium/high/block Phase 3 — Notifications + Frontend: - Notification worker scaffolding (Twilio/SES stubs, retry/backoff) - Nuxt 3 frontend with Tailwind dark theme + brand green - Live monitor via WebSocket with auto-reconnect - Activity history endpoint backfills monitor with RSVPs + scored access checks (including blocked attempts) UX polish: - Marketing-friendly landing page (hero mockup, how-it-works, features, use cases, testimonials, FAQ, final CTA) - Animated layered card mockups on landing + new-event page - Plus-ones stepper, RSVP status badges, filter buttons - Friendly access-check labels (Verified/Review/Suspicious/Blocked) - Dashboard hydration fix via ClientOnly wrapper Infrastructure: - docker-compose for full local dev (postgres, nats, api, fraud-engine, notifier, frontend) - Multi-stage Dockerfiles, non-root UID 1000 - Integration tests with testcontainers-go Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
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)
|
|
})
|
|
}
|