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
+58
View File
@@ -0,0 +1,58 @@
package storage
import (
"context"
"errors"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/alchemistkay/guestguard/internal/domain"
)
type UserRepo struct {
pool *pgxpool.Pool
}
func NewUserRepo(db *DB) *UserRepo {
return &UserRepo{pool: db.Pool}
}
func (r *UserRepo) Create(ctx context.Context, email, name string) (*domain.User, error) {
const q = `
INSERT INTO users (email, name) VALUES ($1, $2)
RETURNING id, email, name, created_at, updated_at
`
row := r.pool.QueryRow(ctx, q, strings.ToLower(strings.TrimSpace(email)), strings.TrimSpace(name))
u, err := scanUser(row)
if err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return nil, domain.ErrEmailTaken
}
return nil, err
}
return u, nil
}
func (r *UserRepo) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
const q = `SELECT id, email, name, created_at, updated_at FROM users WHERE email = $1`
u, err := scanUser(r.pool.QueryRow(ctx, q, strings.ToLower(strings.TrimSpace(email))))
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, err
}
return u, nil
}
func scanUser(s rowScanner) (*domain.User, error) {
var u domain.User
if err := s.Scan(&u.ID, &u.Email, &u.Name, &u.CreatedAt, &u.UpdatedAt); err != nil {
return nil, err
}
return &u, nil
}