b34715f1520ff4f90a790203357b3e04bca7846e
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3629ab8c79 |
fix(tier2-g): rebrand Security → Gate with event-planner language
The original Block G UI was correct but felt like a firewall config panel.
The audience for GuestGuard is wedding couples, party hosts, and event
planners — not network admins. Three concrete problems:
1. "Fraud detection" / "Risk score" frames every guest as a suspect.
"Gate" is the metaphor every host already uses ("gate the guest
list", "doorman"). It's the same idea, friendlier.
2. Three threshold sliders (0–100) are abstract. Hosts can't reason
about "block at 85" without context. Replaced by four presets —
Relaxed / Balanced / Strict / Very strict — each with a sentence
that explains the kind of event it fits. The sliders survive under
an Advanced disclosure for the rare power user.
3. CIDR notation is gibberish to a layperson. "Trusted networks" is
what they're actually doing. A "Use my current network" button
detects the host's apparent IP, widens IPv4 to /24 (typical home
block), and pre-fills the form. Manual entry stays for cases
where the host knows what they're doing.
Plus two leaks fixed on the guest-facing RSVP page: removed the
"Risk score 72 · high" line from both the confirmation and the
blocked-attempt cards. Guests should never see internal scoring
detail — the blocked message now reads "Something about this
attempt looked off" instead of "suspicious access attempt".
Backend
- New GET /me/public-ip (authed) — echoes the caller's apparent IP
so the frontend's auto-detect button doesn't need a third-party
ipify call
Frontend
- New components/GateCard.vue with preset cards + advanced disclosure
+ trusted-networks UX with auto-detect
- Removed components/SecurityCard.vue
- Event tab nav: Security → Gate. Hash-link alias preserves any
/events/<id>#security bookmarks
- RSVP page: leaked risk-score lines removed; blocked-attempt
message reworded for non-technical guests
Internal storage / API names stay (FraudThresholds, fraud_v2,
/security/* endpoints) — they're never user-visible and renaming
them would be a breaking change for no end-user benefit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b873012191 |
feat(tier2): smarter fraud detection — Block G
Per-event fraud tuning. Hosts can now dial the medium / high / block
boundaries, allowlist trusted networks, and feed verdicts back on
flagged accesses — the seed corpus for a future ML model.
Schema (migration 0011)
- events.fraud_{medium,high,block}_threshold default 30/60/85 so
existing events behave identically until a host changes them
- access_logs.geo_{country,city,lat,lon} for future enrichment
- fraud_feedback table — verdict ('legitimate' | 'suspicious') + note,
PK on access_log_id so re-mark is an upsert
- event_allowlists table — (event_id, ip_cidr) primary key, inet column
so containment checks use the native >>= operator (indexed lookup)
Domain
- FraudThresholds with Valid() + Band() helpers; Default trio echoed
through GET responses so the frontend doesn't duplicate constants
- ParseAllowlistCIDR accepts bare IPs (auto-widens to /32 or /128) and
canonicalises the output (203.0.113.42 → 203.0.113.42/32)
- Event.Thresholds() falls back to defaults if columns weren't
populated yet, so the API never wedges every score into "low"
Storage
- AllowlistRepo: List / Add / Remove + Matches() — the latter pushes
CIDR containment into Postgres rather than streaming rows back
- FeedbackRepo: Record (upserts) + ListForEvent (joined through guests)
- EventRepo.GetThresholds + UpdateThresholds, plus the threshold
columns baked into scanEvent so every event load carries them
- AccessLogRepo.BelongsToEvent — stops a hostile editor on event A
from marking event B's access logs
API
- GET/PUT /events/{id}/security/thresholds (viewer/editor)
- GET/POST/DELETE /events/{id}/security/allowlist
- POST /events/{id}/access-logs/{log_id}/feedback (editor)
- GET /events/{id}/security/feedback
- RSVP scoring path: allowlist short-circuit fires before the fraud
engine; the engine's score is then re-banded against the event's
thresholds (engine.Risk becomes advisory — API is the source of
truth for "what counts as block here")
- CORS Allow-Methods already includes PUT (Block D fix)
Fraud engine
- Single-signal cap: it now takes ≥2 sub-scores of ≥70 to push the
final into HIGH. Fixes the well-known "second visit with a slightly
shifted fingerprint scores 60+" false positive
- Engine band remains advisory; API re-bands using per-event
thresholds before deciding to block
Frontend
- SecurityCard.vue: visual band ribbon (proportional to thresholds),
three sliders with mutual clamping so dragging medium past high
pushes high (not an invalid ordering), reset-to-defaults button,
CIDR allowlist with inline add + per-row remove, verdict-history
inbox. Toast feedback on save/add/remove
- "Security" tab added to the event-detail tab nav (5th tab,
right of Analytics)
- Viewer role hides write affordances; server enforces too
Tests
- Domain: ThresholdsBand, ThresholdsValid, ParseAllowlistCIDR (bare
IP widening + traversal/typo rejection), FraudFeedbackValid
- Integration: thresholds round-trip + invalid ordering rejection,
allowlist CRUD + duplicate 409 + invalid CIDR 400 + IP auto-widen,
feedback record + upsert + cross-tenant 404 + invalid verdict 400,
viewer can read / editor can write / outsider gets 404
- Full integration suite green (315.8s, all 36 top-level tests pass)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e5b187c575 |
feat(tier2): event branding + UX polish — Block D
Backend
- Migration 0010 adds event_branding (one row per event; all fields
nullable so a brand-new event renders with defaults)
- BrandingRepo with COALESCE/NULLIF upsert semantics: nil pointer
preserves the existing value, "" clears the field to NULL
- internal/uploads package: ImageStore interface + LocalFSStore (dev),
pure-stdlib decode + re-encode that strips EXIF and rejects anything
that isn't valid JPEG/PNG. Size cap 2 MB, random 16-byte filenames
- GET /events/{id}/branding (viewer+) returns the row plus the
AllowedFonts list so the frontend picker stays in sync
- PUT /events/{id}/branding (editor+) validates hex colours, font
allowlist, and refuses image URLs whose path doesn't start with
/uploads/ (blocks arbitrary-origin <img> smuggling on guest pages)
- POST /uploads/image (authed) → fresh CDN URL; GET /uploads/{file}
serves with year-long cache (immutable random names)
- GET /access/{token} now embeds the host's branding so the RSVP page
can render in their colours/font with their logo + cover
- docker-compose mounts a named volume for uploads
- Custom-domain sub-block deferred to Tier 3 per the plan
Frontend
- BrandingCard.vue: colour pickers, font dropdown, logo + cover upload
with progressive disclosure, live preview pane that re-renders on
every keystroke
- RSVP page applies branding via CSS vars at the section root, so
primary colour theme + font cascade through every child card. Cover
image renders as a banner above the form; logo lands in the header
- Submit button background switches to var(--brand-primary) when set
- Mounted on the event detail page below the guests block
Plus the small UX fixes from the e2e walkthrough:
- Nav: dropped the top-level "Events" link; the logo doubles as the
home affordance (→ /dashboard when signed in, → / otherwise). Account
+ Billing + Sign out live under a profile dropdown (avatar with
initials, opens on click, closes on outside-click / Esc / route nav)
- Renamed "Back to dashboard" → "Back to events" across event detail,
billing, account, and new-event pages
Tests
- TestBrandingGetReturnsDefaults / TestBrandingPutPersists /
TestBrandingPutRejectsBadInputs / TestUploadAndServeImage /
TestUploadRejectsNonImage — all pass
- Domain tests for IsValidHexColor + IsAllowedFont
- Full integration suite green (176s)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6803d700b4 |
feat(tier2): calendar integration — Block B
After confirming attendance (or revisiting an already-attending RSVP),
guests can add the event to Google, Outlook, Apple, or any iCalendar
client with one click.
- internal/calendar package builds an RFC 5545 VCALENDAR plus the three
provider deep-links from a narrow Event projection. Times are emitted
as Z-suffixed UTC; the UID is deterministic (event-uuid@guestguard)
so re-downloads update the same calendar entry instead of duplicating
- GET /access/{token}/calendar.ics returns the .ics with a slugified
filename in Content-Disposition; same rate-limit class as /access
- GET /access/{token} response now embeds google/outlook/yahoo/ics URLs
so the frontend doesn't re-encode per provider
- AddToCalendar.vue renders the four buttons; shown only when the
guest is attending (declined/maybe don't need a calendar entry)
- Unit tests cover ics field presence, RFC 5545 escaping (comma,
semicolon), CRLF line endings, explicit-end handling, omitted
optionals, provider URL shape, filename slugification
- Integration: ics download returns valid VCALENDAR with the event
UID + name; unknown token returns 404; /access embeds the links
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
39533162bb |
feat(tier2): editable RSVPs — Block A
Guests can revisit their invitation link and change their response
or plus-ones up to 5 times. Each prior state is snapshotted into
`rsvp_revisions` and surfaced to the host via a per-guest history
modal on the event detail page.
- Migration 0007 adds rsvp_revisions + rsvps.edit_count (with down)
- RSVPRepo.Update wraps snapshot+update+counter in one transaction,
FOR UPDATE-locking the row so concurrent edits can't bypass the cap
- PATCH /rsvp/{token} re-runs the fraud check on every edit attempt
(different device on an edit is itself a signal)
- POST /rsvp no longer marks the token used — the link stays valid
so the guest can come back to edit
- GET /access/{token} now embeds the existing RSVP so the frontend
renders an edit form instead of a blank submit form on revisit
- New host endpoint GET /events/{id}/guests/{guest_id}/rsvp/history
- Frontend: rsvp/[token].vue toggles between summary + edit form,
surfaces edits-remaining; dashboard adds a "History" action on
responded guests opening a revision-trail modal
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3f8bc58ca9 |
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> |