feat: build core API, fraud engine, notifier, and frontend

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>
This commit is contained in:
Kwaku Danso
2026-05-11 21:08:56 +01:00
parent f760fc3e21
commit 3f8bc58ca9
89 changed files with 22729 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
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)
})
}