Compare commits
7 Commits
e9060eee5a
...
1ef1125b4a
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ef1125b4a | |||
| cc38815867 | |||
| e357a26abb | |||
| b2ad719252 | |||
|
|
5c1be5657e | ||
|
|
25451ff172 | ||
|
|
4ec15fe35f |
@ -0,0 +1,85 @@
|
|||||||
|
# TASK METADATA
|
||||||
|
|
||||||
|
- **Target Files:** `server/routes/auth_forward.ts`, `ui/db_queries.ts`,
|
||||||
|
`ui/mod.ts`, `server/routes/events.ts`, `server/tests/forward_auth.test.ts`,
|
||||||
|
`server/tests/events.test.ts`
|
||||||
|
- **Core Objective:** Implement Phase 1 of the Event & Session Overhaul: Enable
|
||||||
|
Traefik ForwardAuth guest ingress for scoped event attendees, bridge session
|
||||||
|
custom scopes to the Launchpad UI, and wire audit logging on event seat
|
||||||
|
claims.
|
||||||
|
- **Dependencies:** None.
|
||||||
|
- **Additional Important Notes:** Follow zero-trust default-deny principles for
|
||||||
|
ForwardAuth. Guest sessions minting needs to correctly log to the Merkle audit
|
||||||
|
ledger for SIEM visibility.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Architectural Considerations & Risks
|
||||||
|
|
||||||
|
- **Risks:**
|
||||||
|
- **ForwardAuth Security:** Modifying ForwardAuth to accept `guest` accounts
|
||||||
|
risks inadvertently allowing guests to access applications they are not
|
||||||
|
explicitly scoped for. Strict validation of `auth.customScopes` against the
|
||||||
|
requested `appRecord.name` is critical.
|
||||||
|
- **Launchpad Leakage:** The `getDashboardApps` query modification must be
|
||||||
|
robust. If the UNION query is not correctly structured, it could leak
|
||||||
|
visibility of unregistered or unauthorized apps to guests.
|
||||||
|
- **Audit Logging Integrity:** Failure to capture the audit events properly
|
||||||
|
during the `join` phase would break SIEM visibility. Using the non-blocking
|
||||||
|
`auditWrapper.auditLog` is required to avoid impacting user request latency
|
||||||
|
while ensuring compliance.
|
||||||
|
|
||||||
|
- **Alternatives:**
|
||||||
|
- We considered a bypass in `ui/db_queries.ts` where guests completely skip
|
||||||
|
the `grants` JOIN. However, using a `UNION` is structurally superior as it
|
||||||
|
seamlessly supports both pure guests and regular active users who might hold
|
||||||
|
delegated scoped passes. It provides a unified data retrieval flow.
|
||||||
|
|
||||||
|
### 3. Proposed Implementation
|
||||||
|
|
||||||
|
#### Phase 1: ForwardAuth Guest Ingress (`server/routes/auth_forward.ts`)
|
||||||
|
|
||||||
|
1. **Permit Guest Accounts:** Update the account status validation to allow
|
||||||
|
`user.account_status === 'guest'` in addition to `'active'`.
|
||||||
|
2. **Custom Scopes Validation:** If the user is a guest (or relying on custom
|
||||||
|
scopes), verify that `auth.customScopes` contains the explicit application
|
||||||
|
grant (e.g., `app:${appRecord.name}`). If not, return a 403 Forbidden.
|
||||||
|
3. **Header Injection:** For guest sessions, compute the scopes to inject into
|
||||||
|
`X-Forwarded-Scopes` by taking the comma-joined list of `auth.customScopes`
|
||||||
|
(`auth.customScopes.join(",")`). This propagates the app scope (e.g.,
|
||||||
|
`app:ed-droid`) and role (e.g., `viewer`) to downstream reverse proxies.
|
||||||
|
|
||||||
|
#### Phase 2: Launchpad App Query (`ui/db_queries.ts` & `ui/mod.ts`)
|
||||||
|
|
||||||
|
1. **Function Signature Update:** Update
|
||||||
|
`getDashboardApps(userId: string, isAdmin: boolean, customScopes?: string[])`
|
||||||
|
to accept the session's custom scopes.
|
||||||
|
2. **UNION Query:** Update the non-admin SQL query to use a `UNION`.
|
||||||
|
- The first part queries explicitly granted apps via the `grants` table.
|
||||||
|
- The second part parses the provided `customScopes` array to extract allowed
|
||||||
|
app names (e.g., stripping the `app:` prefix), querying the `apps` table
|
||||||
|
for those matching names, and assigning them a pseudo-role such as
|
||||||
|
`"Guest (Viewer)"` (or extracting the role from the scopes if possible).
|
||||||
|
3. **UI Integration:** In `ui/mod.ts`, pass `auth.customScopes` from the
|
||||||
|
resolved session into `getDashboardApps`.
|
||||||
|
|
||||||
|
#### Phase 3: Join Audit Logging (`server/routes/events.ts`)
|
||||||
|
|
||||||
|
1. **Audit `POST /api/join` (Web):** Immediately after minting the guest
|
||||||
|
session, invoke
|
||||||
|
`auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, { slug: event.slug, name: event.name, seatNumber: event.seats_claimed, method: "web" }, getClientIp(c))`.
|
||||||
|
2. **Audit `GET /join/:slug` (CLI):** Similarly, wire
|
||||||
|
`auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, { slug: event.slug, name: event.name, seatNumber: event.seats_claimed, method: "cli" }, getClientIp(c))`
|
||||||
|
into this endpoint before returning the token.
|
||||||
|
|
||||||
|
#### Phase 4: Unit Testing
|
||||||
|
|
||||||
|
1. **ForwardAuth Tests (`server/tests/forward_auth.test.ts`):**
|
||||||
|
- Add tests simulating a guest session with an `app:{name}` scope to ensure a
|
||||||
|
`200 OK` response with correctly formatted `X-Forwarded-Scopes`.
|
||||||
|
- Add tests simulating a guest session without the necessary `app:{name}`
|
||||||
|
scope to ensure a `403 Forbidden` response.
|
||||||
|
2. **Events Tests (`server/tests/events.test.ts`):**
|
||||||
|
- Ensure the `POST /api/join` and `GET /join/:slug` assertions capture the
|
||||||
|
`auditLog` spy calls and verify the exact payload (`event_seat_claimed`
|
||||||
|
action, `event.id` as resource, correct metadata structure).
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
### 1. TASK METADATA
|
||||||
|
|
||||||
|
- **Target Files:** `server/routes/events.ts`, `ui/components/LoginPage.tsx`,
|
||||||
|
`ui/components/RegisterPage.tsx`, `ui/components/EventJoinPage.tsx`
|
||||||
|
- **Core Objective:** Implement Phase 2 Event Overhaul: Add PIN discovery links,
|
||||||
|
normalize input, apply tiered Valkey rate limiting, and implement NAT-safe
|
||||||
|
idempotent re-entry on join.
|
||||||
|
- **Dependencies:** `server/middleware.ts` (for getClientIp),
|
||||||
|
`server/ratelimit.ts` (for rate limiting logic).
|
||||||
|
- **Additional Important Notes:** Frontend changes should mirror backend
|
||||||
|
normalization. Re-entry must seamlessly return an existing active guest
|
||||||
|
session linked to the correct event without incrementing `seats_claimed`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Architectural Considerations & Risks
|
||||||
|
|
||||||
|
- **Risks:**
|
||||||
|
- If idempotent re-entry is flawed, returning a mismatched event session could
|
||||||
|
allow access to an event a user didn't register for, or incorrectly redirect
|
||||||
|
them. We must strictly verify the existing session is a guest session
|
||||||
|
belonging to the target event.
|
||||||
|
- Rate limiting logic might accidentally block legitimate NAT'ed traffic if
|
||||||
|
thresholds are set too low; 5 fail / 3 success per minute per IP should be
|
||||||
|
monitored.
|
||||||
|
- Normalization on the backend must be implemented securely using robust
|
||||||
|
string replacements to prevent SQL injection or unexpected matching
|
||||||
|
behaviors, especially with PostgreSQL parameterized queries
|
||||||
|
(`sqlWrapper.sql`).
|
||||||
|
|
||||||
|
- **Alternatives:**
|
||||||
|
- For normalization, instead of just runtime stripping, we could normalize
|
||||||
|
upon creation and store both normalized and display values. However,
|
||||||
|
stripping at runtime with `REPLACE(pin_code, '-', '')` and `LOWER(slug)` is
|
||||||
|
an acceptable and low-friction approach for our scale.
|
||||||
|
- Rate limiting could use sliding window algorithms, but Valkey-backed fixed
|
||||||
|
windows via the existing `rateLimitWrapper` are more efficient and standard
|
||||||
|
for our stack.
|
||||||
|
|
||||||
|
### 3. Proposed Implementation
|
||||||
|
|
||||||
|
#### Phase 1: Login & Registration Discovery Links
|
||||||
|
|
||||||
|
- **File:** `ui/components/LoginPage.tsx`
|
||||||
|
- Add a "Join with PIN" link next to the existing "Register with Invite" in
|
||||||
|
the footer links section to ensure unauthenticated users can easily discover
|
||||||
|
the join portal.
|
||||||
|
- **File:** `ui/components/RegisterPage.tsx`
|
||||||
|
- Add a similar "Join with PIN" link to the footer for consistency across
|
||||||
|
entry points.
|
||||||
|
|
||||||
|
#### Phase 2: Input Normalization (Frontend & Backend)
|
||||||
|
|
||||||
|
- **File:** `ui/components/EventJoinPage.tsx`
|
||||||
|
- Enhance the UI script to tolerate input formatting, naturally handling
|
||||||
|
hyphens, spaces, and case differences during submission, providing immediate
|
||||||
|
visual feedback without rejecting valid raw input (e.g. `241881` vs
|
||||||
|
`241-881`).
|
||||||
|
- **File:** `server/routes/events.ts` (`POST /api/join` & `GET /join/:slug`)
|
||||||
|
- Strip whitespace, hyphens, and force lowercase on the incoming code on the
|
||||||
|
backend (`code.trim().replace(/[-\s]/g, '').toLowerCase()`).
|
||||||
|
- Update the `sqlWrapper` query to match normalized inputs:
|
||||||
|
`WHERE (LOWER(slug) = ${code} OR REPLACE(pin_code, '-', '') = ${code})`.
|
||||||
|
|
||||||
|
#### Phase 3: Anti-DoS Rate Limiting
|
||||||
|
|
||||||
|
- **File:** `server/routes/events.ts` (`POST /api/join`)
|
||||||
|
- Use `getClientIp(c)` from our middleware to determine the client IP address.
|
||||||
|
- Mount tiered Valkey rate limits using the keys `ratelimit:join:fail:<ip>`
|
||||||
|
(limit: 5 per 60s) and `ratelimit:join:success:<ip>` (limit: 3 per 60s).
|
||||||
|
- Apply the rate limiting logic prior to executing any database queries to
|
||||||
|
prevent database DoS on brute-force PIN attempts.
|
||||||
|
|
||||||
|
#### Phase 4: NAT-Safe Idempotent Re-entry
|
||||||
|
|
||||||
|
- **File:** `server/routes/events.ts` (`POST /api/join`)
|
||||||
|
- Before claiming a seat and generating a new session, check for an existing
|
||||||
|
valid user session using `getAuthenticatedUser(c)`.
|
||||||
|
- If a valid session exists, query the database to determine if the `user_id`
|
||||||
|
is linked to a guest account for the target event (e.g., username matches
|
||||||
|
`guest_<event.slug>_%`).
|
||||||
|
- If the user is already a guest of this event, bypass the `seats_claimed + 1`
|
||||||
|
update and early-return `{ success: true, sessionId, redirectUrl }`, keeping
|
||||||
|
the existing session intact.
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
# TASK METADATA
|
||||||
|
|
||||||
|
- **Target Files:** `ui/components/SessionsPage.tsx`,
|
||||||
|
`ui/components/sessions/WorkshopDrawer.tsx`,
|
||||||
|
`ui/components/sessions/SessionsScript.tsx`
|
||||||
|
- **Core Objective:** Overhaul visual hierarchy of `/dashboard/sessions` and
|
||||||
|
enforce strict 2-state mutually exclusive rendering in `WorkshopDrawer.tsx`.
|
||||||
|
- **Dependencies:** None.
|
||||||
|
- **Additional Important Notes:** Must use pure vanilla JavaScript for DOM
|
||||||
|
manipulation (no React/framework state). Ensure copy pills trigger accessible
|
||||||
|
notifications (aria-live/toast). Fix double emoji bug in JS. Ensure mobile
|
||||||
|
title wrapping. All changes must pass `ui/ui_scripts.test.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architectural Considerations & Risks
|
||||||
|
|
||||||
|
- **Risks:**
|
||||||
|
- **DOM Manipulation Regression:** Because this UI operates strictly on
|
||||||
|
vanilla JavaScript, changing the layout and grouping DOM elements within new
|
||||||
|
containers (like `#eventCreateState` and `#eventHandoffState`) risks
|
||||||
|
breaking existing DOM ID bindings in `SessionsScript.tsx` if IDs are changed
|
||||||
|
or misplaced. We must strictly preserve all existing DOM IDs.
|
||||||
|
- **Test Compatibility:** The client-side scripts are tested via hermetic
|
||||||
|
`new Function()` execution in `ui/ui_scripts.test.ts` (`validateJsSyntax`).
|
||||||
|
The script modifications must remain structurally valid and strictly avoid
|
||||||
|
any unsupported syntax or external framework dependencies.
|
||||||
|
- **Mobile Layout Breakage:** When fixing the long titles to wrap correctly,
|
||||||
|
we must ensure CSS properties like `word-break: break-word` and
|
||||||
|
`overflow-wrap: break-word` don't inadvertently stretch flex containers
|
||||||
|
horizontally on small screens.
|
||||||
|
|
||||||
|
- **Alternatives:**
|
||||||
|
- The proposed container-based 2-state machine approach for
|
||||||
|
`WorkshopDrawer.tsx` is native and robust. It's the most appropriate
|
||||||
|
solution given our constraints (no client-side frameworks). Using a
|
||||||
|
class-based toggle could also work, but explicit element display
|
||||||
|
manipulation is clearer for the specific 2-state requirement.
|
||||||
|
|
||||||
|
## Proposed Implementation
|
||||||
|
|
||||||
|
### 1. Page Hierarchy Update (`ui/components/SessionsPage.tsx`)
|
||||||
|
|
||||||
|
- Relocate the main header block (containing `<h1>Active Sessions & Passes</h1>`
|
||||||
|
and the `[ 🔑 Delegate Session ]` button) to be the uppermost visible element
|
||||||
|
below the `#status-banner`.
|
||||||
|
- Move the `EventCockpitDeck` component to render below the new header block.
|
||||||
|
- Keep the `SessionTable` and `SessionDeck` at the bottom.
|
||||||
|
|
||||||
|
### 2. WorkshopDrawer 2-State Machine (`ui/components/sessions/WorkshopDrawer.tsx`)
|
||||||
|
|
||||||
|
- Wrap the initial creation form (`#eventForm`), including inputs, submit
|
||||||
|
button, and cancel button, into a new container `div` with
|
||||||
|
`id="eventCreateState"`. Default this container to `display: block`.
|
||||||
|
- Rename or wrap the `#eventHandoffModal` content into a new container `div`
|
||||||
|
with `id="eventHandoffState"`. Default this container to `display: none`.
|
||||||
|
- In `#eventHandoffState`, ensure it contains the 3 cards (PIN + /join, Direct
|
||||||
|
Link, CLI 1-Liner) and update the final button to be a single "Dismiss"
|
||||||
|
button.
|
||||||
|
- Ensure `createdEventTitle` has `overflow-wrap: break-word` and
|
||||||
|
`word-break: break-word` along with a max-width to allow long titles to wrap
|
||||||
|
cleanly on mobile screens.
|
||||||
|
|
||||||
|
### 3. JavaScript Logic Update (`ui/components/sessions/SessionsScript.tsx`)
|
||||||
|
|
||||||
|
- **State Toggling:** Update `handleCreateEvent` so that upon successful
|
||||||
|
creation, it sets
|
||||||
|
`document.getElementById('eventCreateState').style.display = 'none'` and
|
||||||
|
`document.getElementById('eventHandoffState').style.display = 'block'`.
|
||||||
|
- **Drawer Reset:** Ensure when the delegate drawer is closed (via
|
||||||
|
`closeDelegateDrawer` or the new Dismiss button), the states are reset:
|
||||||
|
`#eventCreateState` to block, `#eventHandoffState` to none.
|
||||||
|
- **Emoji Fix:** Remove the hardcoded `'🎟️ '` prefix from the event title
|
||||||
|
injection line:
|
||||||
|
`document.getElementById('createdEventTitle').textContent = ev.name + ' Live!';`
|
||||||
|
- **Accessibility / Toast:** Ensure `copyEventHandoff` function already triggers
|
||||||
|
`showNotice` (which acts as a toast). Verify ARIA roles for the banner if
|
||||||
|
necessary, or just rely on the existing `showNotice` function which is already
|
||||||
|
in place.
|
||||||
|
|
||||||
|
### 4. Quality Gates
|
||||||
|
|
||||||
|
- Run Deno tests: `deno test --allow-all` (specifically validating
|
||||||
|
`ui/ui_scripts.test.ts`).
|
||||||
|
- Run code formatting: `deno fmt`.
|
||||||
Loading…
x
Reference in New Issue
Block a user