auth-yes/tasks/complete/2026-0826.02.jul.story.events.join-reentry-and-limits-2156.ph2.md
google-labs-jules[bot] edbceae0fb feat(events): implement bifurcated join inputs and NAT-safe idempotency
- Adds UI links for joining with PIN in Login and Register pages.
- Normalizes event slugs to lowercase (preserving hyphens) and event PINs to strip all hyphens/spaces to handle raw inputs.
- Implements a pre-check rate limit pattern (`isRateLimited`) to safely enforce a max of 5 failed attempts per IP window (60s) without rate-limiting successful authentications.
- Achieves NAT-safe idempotency in `POST /api/join` by extracting and reusing active event guest sessions instead of blindly incrementing claimed seats on every request.
- Integrates complete test suite coverage for these new constraints.

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
2026-08-26 23:57:13 +00:00

4.5 KiB

TASK METADATA

  • Target Files: server/routes/events.ts, ui/components/LoginPage.tsx, ui/components/RegisterPage.tsx, ui/components/EventJoinPage.tsx, server/tests/events.test.ts
  • Core Objective: Implement Phase 2 Event Overhaul: Add PIN discovery links, bifurcate slug/PIN input normalization, apply tiered Valkey rate limiting on failed brute-force attempts, and implement NAT-safe idempotent cookie re-entry.
  • Dependencies: server/middleware.ts (for getClientIp), server/ratelimit.ts (for rate limiting logic).
  • Additional Important Notes: Attendee identity must remain 100% IP-agnostic by relying on browser session cookies (NAT-safe for shared WiFi). IP tracking is strictly reserved for throttling failed brute-force attacks and forensic audit logs.

2. Architectural Considerations & Risks

  • Risks:

    • Slug Hyphen Destruction: Event slugs legitimately contain hyphens (e.g., testing-workshop-event-...). Stripping hyphens globally breaks slug lookups. Normalization MUST bifurcate: preserve hyphens for slugs (LOWER(slug)), while stripping hyphens only for PIN matching (REPLACE(pin_code, '-', '')).
    • NAT False-Positives: Never bind attendee identity or successful seats strictly to an IP. Shared conference/classroom WiFi shares one egress IP. Idempotent re-entry must inspect the incoming session_id cookie.
    • Rate Limiter State Machine: Pre-check gates at the route entry must reject exceeded IPs (429), while post-execution increments must distinctly separate failed code attempts (ratelimit:join:fail:<ip>) from successful joins.
  • Alternatives:

    • Storing pre-normalized columns was evaluated and rejected; runtime normalization with LOWER(slug) and REPLACE(pin_code, '-', '') is fast and zero-migration.

3. Proposed Implementation

  1. ui/components/LoginPage.tsx: Add a "Join with PIN" link next to "Register with Invite" in the footer links section.
  2. ui/components/RegisterPage.tsx: Add a similar "Join with PIN" link to the footer for consistency across all unauthenticated entry points.

Phase 2: Bifurcated Input Normalization

  1. ui/components/EventJoinPage.tsx: Enhance the client-side script to accept raw 6 digits (241881) or hyphenated PINs (241-881) and slugs without throwing client-side validation errors.
  2. server/routes/events.ts (POST /api/join & GET /join/:slug):
    • Create two normalized representations:
      • rawNormalized = code.trim().toLowerCase() (preserves hyphens for slugs).
      • pinNormalized = code.trim().replace(/[-\s]/g, '') (strips hyphens/spaces for PINs).
    • Update SQL query: WHERE (LOWER(slug) = ${rawNormalized} OR REPLACE(pin_code, '-', '') = ${pinNormalized}) AND is_active = TRUE AND (expires_at IS NULL OR expires_at > NOW())

Phase 3: Anti-DoS Rate Limiting on POST /api/join

  1. Pre-Check Gate (Top of Route):
    • Resolve IP via const clientIp = getClientIp(c).
    • Check if ratelimit:join:fail:${clientIp} exceeds 5 attempts per 60s. If so, return 429 Too Many Requests.
  2. Post-Execution Increments:
    • If lookup returns 0 rows (invalid code / expired): increment ratelimit:join:fail:${clientIp} (window: 60s) before returning 404.
    • If seat is successfully claimed: optionally increment ratelimit:join:success:${clientIp} (limit: 3 per 60s).

Phase 4: NAT-Safe Idempotent Re-entry

  1. Session Cookie Inspection (POST /api/join):
    • Before executing the seats_claimed + 1 update, check getAuthenticatedUser(c).
    • If a valid session exists, verify if the session is a guest session belonging to the matched event slug (username.startsWith('guest_' + event.slug + '_')).
    • If matched:
      • Re-issue the session_id cookie with fresh TTL.
      • Return { success: true, sessionId: auth.sessionId, redirectUrl, reused: true } without incrementing seats_claimed.

Phase 5: Unit Testing

  1. server/tests/events.test.ts:
    • Add test: POST /api/join - normalizes PIN without hyphens (241881 -> 241-881).
    • Add test: POST /api/join - normalizes slug case-insensitively while preserving hyphens.
    • Add test: POST /api/join - idempotent re-entry returns existing session without burning seat.
    • Add test: POST /api/join - enforces 5 failed attempts rate limit per IP.