# 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:`) 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 ### Phase 1: Login & Registration Discovery Links 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`.