102 lines
4.5 KiB
Markdown
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`.
|