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
+69
View File
@@ -0,0 +1,69 @@
from __future__ import annotations
import asyncio
import logging
from datetime import UTC, datetime
from uuid import UUID
import grpc
from app.schemas import AccessAttempted
from app.scoring import BLOCK, HIGH, LOW, MEDIUM, HeuristicScorer, risk_band
from fraud.v1 import fraud_pb2, fraud_pb2_grpc
logger = logging.getLogger(__name__)
_RISK_TO_PROTO = {
LOW: fraud_pb2.RISK_LOW,
MEDIUM: fraud_pb2.RISK_MEDIUM,
HIGH: fraud_pb2.RISK_HIGH,
BLOCK: fraud_pb2.RISK_BLOCK,
}
class FraudServicer(fraud_pb2_grpc.FraudServiceServicer):
def __init__(self, scorer: HeuristicScorer) -> None:
self._scorer = scorer
async def Score( # noqa: N802 — gRPC method
self,
request: fraud_pb2.ScoreRequest,
context: grpc.aio.ServicerContext,
) -> fraud_pb2.ScoreResponse:
try:
evt = AccessAttempted(
event_id=UUID(request.event_id),
guest_id=UUID(request.guest_id),
token_id=UUID(request.token_id),
access_log_id=UUID(request.access_log_id) if request.access_log_id else UUID(int=0),
fingerprint=dict(request.fingerprint) if request.fingerprint else None,
ip_address=request.ip_address or None,
user_agent=request.user_agent or None,
referrer=request.referrer or None,
occurred_at=datetime.now(UTC),
)
except (ValueError, TypeError) as exc:
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"bad request: {exc}")
raise # unreachable, abort raises
result = self._scorer.score(evt)
band = risk_band(result.score)
return fraud_pb2.ScoreResponse(
score=result.score,
risk=_RISK_TO_PROTO.get(band, fraud_pb2.RISK_UNSPECIFIED),
reasons=result.reasons,
)
async def serve_grpc(scorer: HeuristicScorer, addr: str) -> grpc.aio.Server:
server = grpc.aio.server()
fraud_pb2_grpc.add_FraudServiceServicer_to_server(FraudServicer(scorer), server)
server.add_insecure_port(addr)
await server.start()
logger.info("grpc server started", extra={"addr": addr})
return server
async def stop_grpc(server: grpc.aio.Server) -> None:
await server.stop(grace=2.0)
await asyncio.sleep(0)