auth-yes/tasks/new/2026-0826.02.jul.story.events.join-reentry-and-limits-2156.ph2.md

102 lines
4.5 KiB
Markdown

# 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
### 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`.