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
+122
View File
@@ -0,0 +1,122 @@
package notification
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/alchemistkay/guestguard/internal/storage"
)
type Channel string
const (
ChannelSMS Channel = "sms"
ChannelEmail Channel = "email"
)
type Type string
const (
TypeInvitation Type = "invitation"
TypeVerification Type = "verification"
TypeConfirmation Type = "confirmation"
TypeReminder Type = "reminder"
)
type Status string
const (
StatusQueued Status = "queued"
StatusSent Status = "sent"
StatusDelivered Status = "delivered"
StatusFailed Status = "failed"
)
// Sender is the boundary between the worker and a real provider (Twilio,
// SES, etc). Phase 3 ships a logging implementation; later phases swap it
// out without touching consumer code.
type Sender interface {
Send(ctx context.Context, msg OutboundMessage) (providerID string, err error)
}
type OutboundMessage struct {
GuestID uuid.UUID
Channel Channel
Type Type
Subject string
Body string
Metadata map[string]any
}
// Repo persists notification records.
type Repo struct {
pool *pgxpool.Pool
}
func NewRepo(db *storage.DB) *Repo {
return &Repo{pool: db.Pool}
}
type RecordParams struct {
GuestID uuid.UUID
Channel Channel
Type Type
Status Status
ProviderID string
Error string
}
func (r *Repo) Record(ctx context.Context, p RecordParams) (uuid.UUID, error) {
var providerID *string
if p.ProviderID != "" {
providerID = &p.ProviderID
}
var errStr *string
if p.Error != "" {
errStr = &p.Error
}
var deliveredAt *time.Time
if p.Status == StatusSent || p.Status == StatusDelivered {
now := time.Now().UTC()
deliveredAt = &now
}
const q = `
INSERT INTO notifications (guest_id, channel, type, status, provider_id,
attempts, last_attempt, delivered_at, error)
VALUES ($1, $2, $3, $4, $5, 1, now(), $6, $7)
RETURNING id
`
var id uuid.UUID
err := r.pool.QueryRow(ctx, q,
p.GuestID, string(p.Channel), string(p.Type), string(p.Status),
providerID, deliveredAt, errStr,
).Scan(&id)
if err != nil {
return uuid.Nil, fmt.Errorf("record notification: %w", err)
}
return id, nil
}
// LogSender pretends to send and just logs. Useful for Phase 3 demos and
// tests; concrete providers (Twilio/SES) plug in later.
type LogSender struct{}
func (LogSender) Send(_ context.Context, msg OutboundMessage) (string, error) {
if msg.GuestID == uuid.Nil {
return "", errors.New("missing guest id")
}
meta, _ := json.Marshal(msg.Metadata)
providerID := "log:" + uuid.NewString()
// We deliberately don't write to stdout here; the worker emits the slog
// line so we control the structure.
_ = meta
return providerID, nil
}