diff --git a/.github/workflows/cd-backend.yml b/.github/workflows/cd-backend.yml index db2575a..b7254e5 100644 --- a/.github/workflows/cd-backend.yml +++ b/.github/workflows/cd-backend.yml @@ -52,6 +52,5 @@ jobs: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID_BACKEND }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_BACKEND }} run: | - cd output vercel pull --yes --environment=production --token=$VERCEL_TOKEN - vercel deploy --prod --token=$VERCEL_TOKEN --force \ No newline at end of file + vercel deploy --prod --token=$VERCEL_TOKEN --force diff --git a/shatter-backend/api/index.ts b/shatter-backend/api/index.ts index 655999c..5047b37 100644 --- a/shatter-backend/api/index.ts +++ b/shatter-backend/api/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import mongoose from 'mongoose'; -import app from '../src/app'; +import app from '../src/app.js'; const MONGODB_URI = process.env.MONGO_URI; @@ -56,7 +56,7 @@ async function connectDB() { connectDB().catch(console.error); -app.use(async (req, res, next) => { +app.use(async (req: any, res: any, next: any) => { try { await connectDB(); next(); diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index f1f50a8..a51ac2f 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -1,48 +1,61 @@ # Shatter Backend — API Reference -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 **Base URL:** `http://localhost:4000/api` --- ## Table of Contents -- [General Information](#general-information) -- [Endpoint Summary](#endpoint-summary) -- [Authentication (`/api/auth`)](#authentication-apiauth) - - [POST /api/auth/signup](#post-apiauthsignup) - - [POST /api/auth/login](#post-apiauthlogin) - - [GET /api/auth/linkedin](#get-apiauthlinkedin) - - [GET /api/auth/linkedin/callback](#get-apiauthlinkedincallback) - - [POST /api/auth/exchange](#post-apiauthexchange) -- [Users (`/api/users`)](#users-apiusers) - - [GET /api/users](#get-apiusers) - - [POST /api/users](#post-apiusers) - - [GET /api/users/me](#get-apiusersme) - - [GET /api/users/:userId](#get-apiusersuserid) - - [GET /api/users/:userId/events](#get-apiusersuseриdevents) - - [PUT /api/users/:userId](#put-apiusersuserid) -- [Events (`/api/events`)](#events-apievents) - - [POST /api/events/createEvent](#post-apieventscreateevent) - - [GET /api/events/event/:joinCode](#get-apieventseventjoincode) - - [GET /api/events/:eventId](#get-apieventseventid) - - [PUT /api/events/:eventId/status](#put-apieventseventidstatus) - - [POST /api/events/:eventId/join/user](#post-apieventseventиdjoinuser) - - [POST /api/events/:eventId/join/guest](#post-apieventseventиdjoinguest) - - [GET /api/events/createdEvents/user/:userId](#get-apieventscreatedeventsuseriduserid) -- [Bingo (`/api/bingo`)](#bingo-apibingo) - - [POST /api/bingo/createBingo](#post-apibingocreatebingo) - - [GET /api/bingo/getBingo/:eventId](#get-apibingogetbingoeventid) - - [PUT /api/bingo/updateBingo](#put-apibingoupdatebingo) -- [Participant Connections (`/api/participantConnections`)](#participant-connections-apiparticipantconnections) - - [POST /api/participantConnections/](#post-apiparticipantconnections) - - [POST /api/participantConnections/by-emails](#post-apiparticipantconnectionsby-emails) - - [DELETE /api/participantConnections/delete](#delete-apiparticipantconnectionsdelete) - - [GET /api/participantConnections/getByParticipantAndEvent](#get-apiparticipantconnectionsgetbyparticipantandevent) - - [GET /api/participantConnections/getByUserEmailAndEvent](#get-apiparticipantconnectionsgetbyuseremailandevent) - - [GET /api/participantConnections/connected-users](#get-apiparticipantconnectionsconnected-users) -- [Planned Endpoints](#planned-endpoints-) -- [Quick Start Examples](#quick-start-examples) +- [Shatter Backend — API Reference](#shatter-backend--api-reference) + - [Table of Contents](#table-of-contents) + - [Endpoint Summary](#endpoint-summary) + - [General Information](#general-information) + - [Authentication](#authentication) + - [Response Format](#response-format) + - [Common Status Codes](#common-status-codes) + - [Authentication (`/api/auth`)](#authentication-apiauth) + - [POST `/api/auth/signup`](#post-apiauthsignup) + - [POST `/api/auth/login`](#post-apiauthlogin) + - [GET `/api/auth/linkedin`](#get-apiauthlinkedin) + - [GET `/api/auth/linkedin/callback`](#get-apiauthlinkedincallback) + - [POST `/api/auth/exchange`](#post-apiauthexchange) + - [Users (`/api/users`)](#users-apiusers) + - [GET `/api/users`](#get-apiusers) + - [POST `/api/users`](#post-apiusers) + - [GET `/api/users/me`](#get-apiusersme) + - [GET `/api/users/:userId`](#get-apiusersuserid) + - [GET `/api/users/:userId/events`](#get-apiusersuseridevents) + - [PUT `/api/users/:userId`](#put-apiusersuserid) + - [Events (`/api/events`)](#events-apievents) + - [POST `/api/events/createEvent`](#post-apieventscreateevent) + - [GET `/api/events/event/:joinCode`](#get-apieventseventjoincode) + - [GET `/api/events/:eventId`](#get-apieventseventid) + - [PUT `/api/events/:eventId/status`](#put-apieventseventidstatus) + - [POST `/api/events/:eventId/join/user`](#post-apieventseventidjoinuser) + - [POST `/api/events/:eventId/join/guest`](#post-apieventseventidjoinguest) + - [GET `/api/events/createdEvents/user/:userId`](#get-apieventscreatedeventsuseruserid) + - [Bingo (`/api/bingo`)](#bingo-apibingo) + - [POST `/api/bingo/createBingo`](#post-apibingocreatebingo) + - [GET `/api/bingo/getBingo/:eventId`](#get-apibingogetbingoeventid) + - [PUT `/api/bingo/updateBingo`](#put-apibingoupdatebingo) + - [POST `/api/bingo/generate`](#post-apibingogenerate) + - [Participant Connections (`/api/participantConnections`)](#participant-connections-apiparticipantconnections) + - [POST `/api/participantConnections/`](#post-apiparticipantconnections) + - [POST `/api/participantConnections/by-emails`](#post-apiparticipantconnectionsby-emails) + - [DELETE `/api/participantConnections/delete`](#delete-apiparticipantconnectionsdelete) + - [GET `/api/participantConnections/getByParticipantAndEvent`](#get-apiparticipantconnectionsgetbyparticipantandevent) + - [GET `/api/participantConnections/getByUserEmailAndEvent`](#get-apiparticipantconnectionsgetbyuseremailandevent) + - [GET `/api/participantConnections/connected-users`](#get-apiparticipantconnectionsconnected-users) + - [Planned Endpoints ⏳](#planned-endpoints-) + - [Quick Start Examples](#quick-start-examples) + - [1. Sign up](#1-sign-up) + - [2. Log in](#2-log-in) + - [3. Create an event](#3-create-an-event) + - [4. Join the event (as authenticated user)](#4-join-the-event-as-authenticated-user) + - [5. Join the event (as guest)](#5-join-the-event-as-guest) + - [6. Create a bingo game for the event](#6-create-a-bingo-game-for-the-event) + - [7. Get the bingo game](#7-get-the-bingo-game) --- @@ -169,7 +182,8 @@ Create a new user account. | 400 | `"name, email and password are required"` | | 400 | `"Invalid email format"` | | 400 | `"Password must be at least 8 characters long"` | -| 409 | `"Email already exists"` | +| 409 | `"Email already exists"` (local account) | +| 409 | `"This email is associated with a LinkedIn account. Please log in with LinkedIn."` (LinkedIn account) | --- @@ -420,12 +434,18 @@ Get all events a user has joined (populates event details). "joinCode": "12345678", "startDate": "2025-02-01T18:00:00.000Z", "endDate": "2025-02-01T21:00:00.000Z", - "currentState": "In Progress" + "currentState": "In Progress", + "participantIds": [ + { "_id": "666b...", "name": "John Doe", "userId": "664f..." }, + { "_id": "666c...", "name": "Jane Smith", "userId": "664e..." } + ] } ] } ``` +**Note:** Each event's `participantIds` is populated with participant `name` and `userId` fields, enabling the frontend to load participant connections. + **Error Responses:** | Status | Error | @@ -456,6 +476,8 @@ Update a user's profile. Users can only update their own profile. | `bio` | string | | | `profilePhoto` | string | URL | | `socialLinks` | object | `{ linkedin?, github?, other? }` | +| `organization` | string | Where the user works/studies | +| `title` | string | Job title or role | **Success Response (200):** @@ -716,13 +738,13 @@ Join an event as a registered (authenticated) user. | 404 | `"User not found"` | | 404 | `"Event not found"` | | 409 | `"User already joined"` | -| 409 | `"This name is already taken in this event"` | **Special Behavior:** - Creates a Participant record linking user to event +- If the display name is already taken in the event, a `#XXX` suffix is automatically appended (e.g., `John` becomes `John#472`). The response `participant.name` reflects the final display name. - Adds participant to event's `participantIds` array - Adds event to user's `eventHistoryIds` array -- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` +- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` (using the final display name) --- @@ -740,9 +762,18 @@ Join an event as a guest (no account required). **Request Body:** -| Field | Type | Required | -|--------|--------|----------| -| `name` | string | Yes | +| Field | Type | Required | Description | +|--------|--------|----------|-------------| +| `name` | string | Yes | Display name | +| `email` | string | No* | Email address | +| `socialLinks` | object | No* | Social links | +| `socialLinks.linkedin` | string | No | LinkedIn URL | +| `socialLinks.github` | string | No | GitHub URL | +| `socialLinks.other` | string | No | Other URL | +| `organization` | string | No* | Where the guest works/studies | +| `title` | string | No | Job title or role | + +\* At least one of the following is required: `email`, a non-empty field in `socialLinks`, or `organization`. **Success Response (200):** @@ -765,15 +796,18 @@ Join an event as a guest (no account required). | Status | Error | |--------|-------| | 400 | `"Missing fields: guest name and eventId are required"` | +| 400 | `"At least one contact method (email or social link) or organization is required"` | +| 400 | `"Invalid email format"` | | 400 | `"Event is full"` | | 404 | `"Event not found"` | -| 409 | `"This name is already taken in this event"` | +| 409 | `"A user with this email already exists"` | **Special Behavior:** -- Creates a guest User (`authProvider: 'guest'`, no email/password) +- Creates a guest User (`authProvider: 'guest'`) with the provided contact info (email, social links, and/or organization) +- If the display name is already taken in the event, a `#XXX` suffix is automatically appended (e.g., `John` becomes `John#472`). The response `participant.name` reflects the final display name, and the guest User's name is updated to match. - Returns a JWT so the guest can make authenticated requests - Guest can later upgrade to a full account via `PUT /api/users/:userId` -- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` +- Triggers Pusher event `participant-joined` on channel `event-{eventId}` with payload `{ participantId, name }` (using the final display name) --- @@ -827,9 +861,9 @@ Create a bingo game for an event. | Field | Type | Required | Notes | |---------------|------------|----------|-------| -| `_eventId` | ObjectId | Yes | Must reference an existing event | -| `description` | string | No | | -| `grid` | string[][] | No | 2D array of strings | +| `_eventId` | ObjectId | Yes | Must reference an existing event | +| `description` | string | No | | +| `grid` | BingoTile\[\]\[\] | No | 2D array of `{ question: string, shortQuestion: string }` | **Success Response (201):** @@ -842,9 +876,16 @@ Create a bingo game for an event. "_eventId": "665a...", "description": "Networking Bingo", "grid": [ - ["Has a pet", "Speaks 3 languages", "Loves hiking"], - ["Works remotely", "Free space", "Plays guitar"], - ["From another country", "Has a blog", "Codes in Rust"] + [ + { "question": "Has a pet", "shortQuestion": "Has pet" }, + { "question": "Speaks 3 languages", "shortQuestion": "Speaks languages" }, + { "question": "Loves hiking outdoors", "shortQuestion": "Loves hiking" } + ], + [ + { "question": "Works remotely full-time", "shortQuestion": "Works remotely" }, + { "question": "Free space", "shortQuestion": "Free space" }, + { "question": "Plays guitar regularly", "shortQuestion": "Plays guitar" } + ] ] } } @@ -856,7 +897,7 @@ Create a bingo game for an event. |--------|-------| | 400 | `"_eventId is required"` | | 400 | `"_eventId must be a valid ObjectId"` | -| 400 | `"grid must be a 2D array of strings"` | +| 400 | `"grid must be a 2D array of { question: string, shortQuestion: string }"` | | 404 | `"Event not found"` | --- @@ -882,7 +923,12 @@ Get bingo by event ID (or bingo ID). "_id": "bingo_a1b2c3d4", "_eventId": "665a...", "description": "Networking Bingo", - "grid": [["Has a pet", "Speaks 3 languages", ...], ...] + "grid": [ + [ + { "question": "Has a pet", "shortQuestion": "Has pet" }, + { "question": "Speaks 3 languages", "shortQuestion": "Speaks languages" } + ] + ] } } ``` @@ -905,9 +951,9 @@ Update a bingo game. | Field | Type | Required | Notes | |---------------|------------|----------|-------| -| `id` | string | Yes | Bingo `_id` or event `_eventId` | -| `description` | string | No | | -| `grid` | string[][] | No | 2D array of strings | +| `id` | string | Yes | Bingo `_id` or event `_eventId` | +| `description` | string | No | | +| `grid` | BingoTile\[\]\[\] | No | 2D array of `{ question: string, shortQuestion: string }` | **Success Response (200):** @@ -924,7 +970,7 @@ Update a bingo game. |--------|-------| | 400 | `"id is required"` | | 400 | `"description must be a string"` | -| 400 | `"grid must be a 2D array of strings"` | +| 400 | `"grid must be a 2D array of { question: string, shortQuestion: string }"` | | 400 | `"Nothing to update: provide description and/or grid"` | | 404 | `"Bingo not found"` | @@ -932,6 +978,59 @@ Update a bingo game. --- +### POST `/api/bingo/generate` + +Generate an AI-powered bingo grid based on a given context. + +- **Auth:** Protected + +**Request Body:** + +| Field | Type | Required | Notes | +|-----------|--------|----------|-------| +| `context` | string | Yes | Context used to generate bingo content | +| `n_rows` | number | Yes | Number of rows (1–5) | +| `n_cols` | number | Yes | Number of columns (1–5) | + +**Example Request:** + +```json +{ + "context": "Software engineer networking event where developers meet, discuss tech stacks, exchange ideas, talk about startups, open source, AI, and career opportunities", + "n_rows": 2, + "n_cols": 2 +} +``` + +**Example Response:** +``` +{ + "status": true, + "bingo_grid": [ + [ + { + "question": "Sketches architecture on a napkin", + "shortQuestion": "Napkin architecture" + }, + { + "question": "Shows a product demo on phone", + "shortQuestion": "Phone product demo" + } + ], + [ + { + "question": "Explains their open-source contribution", + "shortQuestion": "Open-source contribution" + }, + { + "question": "Asks 'What's your current stack?'", + "shortQuestion": "Current stack question" + } + ] + ] +} +``` + ## Participant Connections (`/api/participantConnections`) ### POST `/api/participantConnections/` @@ -1242,7 +1341,8 @@ curl -X POST http://localhost:4000/api/events//join/user \ curl -X POST http://localhost:4000/api/events//join/guest \ -H "Content-Type: application/json" \ -d '{ - "name": "Guest User" + "name": "Guest User", + "email": "guest@example.com" }' ``` diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index 97b840b..e68af11 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -1,6 +1,6 @@ # Shatter Backend — Database Schema Reference -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 **Database:** MongoDB with Mongoose ODM **Collections:** 6 @@ -63,6 +63,8 @@ | `passwordHash` | String | No | — | `select: false` — excluded from queries by default | | `linkedinId` | String | No | — | Unique (sparse) | | `linkedinUrl` | String | No | — | Unique (sparse) | +| `organization` | String | No | — | Trimmed | +| `title` | String | No | — | Trimmed | | `bio` | String | No | — | Trimmed | | `profilePhoto` | String | No | — | | | `socialLinks` | Object | No | — | `{ linkedin?: String, github?: String, other?: String }` | @@ -157,6 +159,7 @@ ### Key Behaviors - The compound unique index on `(eventId, name)` is case-insensitive, so "John" and "john" are treated as the same name within an event. +- When a name collision occurs during join, the backend automatically appends a random `#XXX` suffix (e.g., `John#472`) and retries, allowing multiple participants with the same base name. - No timestamps are enabled on this model. --- @@ -174,7 +177,7 @@ | `_id` | String | Auto | Auto | Custom: `bingo_<8 random chars>` | | `_eventId` | ObjectId | Yes | — | Refs `Event` | | `description` | String | No | — | | -| `grid` | [[String]] | No | — | 2D array of strings | +| `grid` | [[{ question: String, shortQuestion: String }]] | No | — | 2D array of BingoTile objects | ### Pre-Save Hooks diff --git a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md index 987aeea..07641e4 100644 --- a/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md +++ b/shatter-backend/docs/REALTIME_EVENTS_GUIDE.md @@ -1,6 +1,6 @@ # Shatter Backend — Real-Time Events Guide -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 --- @@ -106,7 +106,7 @@ Each event has its own channel. Subscribe when a user enters an event, unsubscri | Field | Type | Description | |-----------------|----------|-------------| | `participantId` | ObjectId | The new participant's ID | -| `name` | string | The participant's display name | +| `name` | string | The participant's display name (may include a `#XXX` suffix if the name was already taken in the event, e.g., `"John#472"`) | **Use case:** Update the live participant list in the event lobby/dashboard without polling. diff --git a/shatter-backend/docs/name-bingo-roadmap.md b/shatter-backend/docs/name-bingo-roadmap.md new file mode 100644 index 0000000..ede1783 --- /dev/null +++ b/shatter-backend/docs/name-bingo-roadmap.md @@ -0,0 +1,1082 @@ +# Name Bingo Backend Roadmap + +This roadmap outlines backend development tasks for the Name Bingo feature. Tasks are organized by priority and dependency order. + +**Scope**: Backend API development only. Frontend (mobile/web) tasks are out of scope but noted as dependencies. + +--- + +## Verification Report + +This roadmap has been cross-referenced against: +- `feature_list.md` (MVP requirements) +- `bingo_walkthrough.md` (user flow requirements) +- Mobile app implementation (`mobile-guest-refactor` branch) + +### Coverage Summary + +| Category | Status | +|----------|--------| +| MVP Backend APIs | ~95% (minor gaps addressed below) | +| Bingo Walkthrough | ~90% (Section 10 gaps addressed below) | +| Data Models | Documented explicitly | +| Lifecycle | Fully covered | +| Real-time | Fully covered | +| Auth | Partially covered (LinkedIn OAuth complete, LinkedIn account linking for guests missing) | +| Participant Connections | Fully implemented (model, CRUD, routes — access control for guests missing) | +| Guest Account Upgrade | Partially covered (email/password upgrade works, LinkedIn linking missing) | +| Mobile Bingo Gameplay | Client-side implementation complete (see Mobile Bingo Implementation Status) | + +### Gaps Addressed in This Version + +| Gap | Resolution | Location | +|-----|------------|----------| +| GET /api/users/:userId (Get Profile) | Added | Phase 3.1 | +| GET /api/users/:userId/current-event | Added | Phase 3.5 | +| profilePhoto in participant search | Added | Phase 6.5 | +| Rate limiting middleware | Added | Phase 6.10 | +| allowFreeTextEntry config | Deferred to Phase 6 | Phase 6.6 | +| gridSize in Bingo model | Deferred to Phase 6 | Phase 6.6 | +| Input sanitization | Added to Phase 4.1 | +| Guest account upgrade via LinkedIn linking | Added | Phase 0.3 Task B | +| Connections access control for guest users | Added | Phase 0.3 Task C | +| Organizer Analytics endpoints (feature_list.md "Wants") | Added as future item | Phase 6.11 | +| `gameType` and `eventImg` fields on Event model | Added | Phase 1.1 | +| `currentState` enum matching mobile `EventState` | Added | Phase 1.2 | +| Event status transition API (priority raised) | Moved from Phase 2 to Phase 1 | Phase 1.3 | + +--- + +## Current Implementation Status + +| Component | Status | Location | +|-----------|--------|----------| +| Authentication (signup/login) | Complete | `auth_controller.ts`, `auth_routes.ts` | +| JWT Middleware | Complete | `auth_middleware.ts`, `jwt_utils.ts` | +| User Model & CRUD | Complete | `user_model.ts`, `user_controller.ts` | +| Event Create/Join/Get | Complete | `event_controller.ts`, `event_model.ts` | +| Participant Model | Complete | `participant_model.ts` | +| Guest Join Flow (with User creation, `#XXX` name suffix on collision) | Complete | `event_controller.ts` | +| Profile Update Endpoint | Complete | `user_controller.ts`, `user_route.ts` | +| Pusher Real-time Setup | Complete | `pusher_websocket.ts` | +| Bingo CRUD (basic) | Complete | `bingo_controller.ts`, `bingo_model.ts` | +| LinkedIn OAuth | Complete | `linkedin_oauth.ts`, `auth_controller.ts` | +| Auth Code Exchange | Complete | `auth_code_model.ts`, `auth_controller.ts` | +| Quick Signup (Guest Join) | Complete | `event_controller.ts`, `user_model.ts` | +| ParticipantConnection Model | Complete | `participant_connection_model.ts` | +| ParticipantConnection CRUD | Complete | `participant_connections_controller.ts`, `participant_connections_routes.ts` | +| Guest Upgrade (Email/Password) | Complete | `user_controller.ts` (`updateUser` — sets password, upgrades authProvider) | +| Documentation (API, Real-time, Schema, Lifecycle) | Complete | `docs/API_REFERENCE.md`, `REALTIME_EVENTS_GUIDE.md`, `DATABASE_SCHEMA.md`, `EVENT_LIFECYCLE.md` | +| QR Code Generation | Complete (web client-side) | `shatter-web/src/components/QRCard.tsx` (uses `qrcode.react`, no backend needed) | +| Guest Upgrade (LinkedIn Linking) | Planned | Phase 0.3 Task B | +| Connections Access Control (Guests) | Planned | Phase 0.3 Task C | +| Event Model (`gameType`, `eventImg`) | Complete | Phase 1.1 | +| Event Status Enum (`currentState`) | Complete | Phase 1.2 | +| Event Status Transition API | Complete | Phase 1.3 | +| Player Game State | Deferred (mobile handles client-side) | Phase 6 | +| Participant Search | Deferred (mobile uses existing participant list) | Phase 6 | +| Event Lifecycle Transitions | Complete | Phase 1.3 | + +--- + +## Mobile Bingo Implementation Status + +The `mobile-guest-refactor` branch implements significant client-side bingo gameplay, which reduces the backend work needed for MVP. The following features are handled entirely on the mobile client: + +| Feature | Mobile Implementation | Backend Dependency | +|---------|----------------------|-------------------| +| Bingo grid display | Renders grid from `GET /api/bingo/getBingo/:eventId` categories | Existing endpoint (works) | +| Name assignment to cards | Autocomplete modal using participant list from event data | Existing `participantIds` on event (works) | +| Duplicate name prevention | Client-side validation — can't assign same name to two cards | None | +| Win detection (rows, cols, diagonals) | Client-side logic checks all win conditions | None | +| Blackout detection & animation | Client-side check for all cells filled + animation | None | +| Game state persistence | AsyncStorage — survives app restart | None | +| Lobby → game transition | Polls event status, transitions when `currentState` changes | **Needs Phase 1.2 + 1.3** | +| Event image display | Renders `eventImg` from event data | **Needs Phase 1.1** | +| Game type display | Renders `gameType` from event data | **Needs Phase 1.1** | + +### Mobile Enum Values (source of truth: `shatter-mobile/src/interfaces/Event.tsx`) + +```typescript +export enum EventState { + UPCOMING = "Upcoming", + IN_PROGRESS = "In Progress", + COMPLETED = "Completed", + INVALID = "Invalid", +} + +export enum GameType { + NAME_BINGO = "Name Bingo" +} +``` + +**Backend must use these exact string values** for `currentState` and `gameType` enums to maintain compatibility. + +--- + +## Phase 0: Authentication Enhancements (Critical Priority) + +These features are specified in `bingo_walkthrough.md` Section 3.1 as primary authentication methods. + +### 0.1 LinkedIn OAuth Integration ✅ COMPLETE + +**Endpoints**: +- `GET /api/auth/linkedin` - Initiates OAuth flow (redirects to LinkedIn) +- `GET /api/auth/linkedin/callback` - OAuth callback (creates/updates user, redirects with auth code) +- `POST /api/auth/exchange` - Exchange single-use auth code for JWT token + +**Implementation** (custom, no Passport dependency): +- `src/utils/linkedin_oauth.ts` - LinkedIn API helpers (`getLinkedInAuthUrl`, `getLinkedInAccessToken`, `getLinkedInProfile`) +- `src/models/auth_code_model.ts` - Single-use auth code model (60s TTL, auto-expires) +- `src/controllers/auth_controller.ts` - `linkedinAuth`, `linkedinCallback`, `exchangeAuthCode` +- `src/routes/auth_routes.ts` - All routes registered + +**User Model Fields** (in `user_model.ts`): +- `linkedinId` (String, unique, sparse) - LinkedIn subject ID +- `linkedinUrl` (String, unique, sparse) - LinkedIn profile URL +- `authProvider` (Enum: 'local' | 'linkedin', default 'local') +- `profilePhoto` (String) - populated from LinkedIn picture + +**Security**: +- CSRF protection via JWT-encoded state token (5-minute expiry) +- Auth code is single-use (atomic `findOneAndDelete`) with 60-second TTL +- JWT token never exposed in redirect URLs +- Email conflict detection (prevents duplicate accounts) + +**Env Vars**: `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, `LINKEDIN_CALLBACK_URL`, `FRONTEND_URL` + +**Frontend Integration**: +1. Open browser to `GET /api/auth/linkedin` +2. User authenticates with LinkedIn +3. Backend redirects to `{FRONTEND_URL}/auth/callback?code=` +4. Frontend calls `POST /api/auth/exchange` with `{ "code": "" }` +5. Response: `{ "message": "Authentication successful", "userId": "...", "token": "..." }` + +--- + +### 0.2 Quick Signup via Guest Join ✅ COMPLETE + +**Approach**: Instead of a separate signup endpoint, guest users are created automatically when joining an event. + +**Endpoint**: `POST /api/events/:eventId/join/guest` +- Takes only `{ name }` in request body +- Creates a User record with `authProvider: 'guest'` (no email/password required) +- Returns `{ success, participant, userId, token }` — guest gets a JWT immediately + +**Profile Completion**: `PUT /api/users/:userId` (protected, self-only) +- Guests can add email, password, bio, profilePhoto, socialLinks later +- Setting a password upgrades `authProvider` from `'guest'` to `'local'` +- Email validated for format and uniqueness, password must be >= 8 chars + +**Model Changes** (`user_model.ts`): +- `email` is now optional with `sparse: true` (allows multiple guest users without email) +- `authProvider` enum: `'local' | 'linkedin' | 'guest'` +- Added `bio` (String) and `socialLinks` (linkedin, github, other) fields + +**Implementation**: +- `src/controllers/event_controller.ts` — upgraded `joinEventAsGuest` +- `src/controllers/user_controller.ts` — added `updateUser` +- `src/routes/user_route.ts` — added `PUT /:userId` route + +**Frontend Dependency**: Mobile app guest join flow + profile completion screen. + +--- + +### 0.3 Guest Account Upgrade Flow (High Priority) + +**Reference**: `bingo_walkthrough.md` Section 10 — Guest Account Completion Flow + +Guest users can join events and play games without interruption, but they cannot access saved connections until they upgrade their account. The backend must support two upgrade paths and enforce connections access control. + +#### Task A: Email/Password Upgrade ✅ COMPLETE + +**Endpoint**: `PUT /api/users/:userId` (existing, protected, self-only) + +Already implemented in `user_controller.ts` (`updateUser`): +- Guest sets `email` + `password` via profile update +- Setting a password automatically upgrades `authProvider` from `'guest'` to `'local'` +- Email validated for format and uniqueness, password must be >= 8 chars + +No additional backend work required. + +#### Task B: LinkedIn Account Linking for Guest Users + +**Endpoint**: `POST /api/auth/linkedin/link` (new, protected) + +**Purpose**: Allow an authenticated guest user to attach their LinkedIn account, upgrading their `authProvider` from `'guest'` to `'linkedin'`. + +**Current gap**: The existing `linkedinCallback` in `auth_controller.ts` rejects requests when an email already exists (duplicate account prevention). It has no logic to link LinkedIn credentials to an existing guest account. + +**Implementation**: +- New endpoint that accepts an authenticated guest user's JWT +- Initiates or completes LinkedIn OAuth and attaches `linkedinId`, `linkedinUrl`, and `profilePhoto` to the existing user +- Updates `authProvider` from `'guest'` to `'linkedin'` +- Alternative approach: Modify the existing `linkedinCallback` to detect when the authenticated user is a guest and link instead of rejecting + +**Request flow**: +1. Authenticated guest user calls `POST /api/auth/linkedin/link` (or is redirected through a linking-specific OAuth flow) +2. Backend exchanges LinkedIn auth code for profile data +3. Backend attaches LinkedIn fields to existing guest user document +4. Returns updated user profile + existing JWT remains valid + +**Security**: +- Only users with `authProvider: 'guest'` can use this endpoint +- LinkedIn account must not already be linked to another user +- Validate JWT and confirm `req.user.userId` matches the account being linked + +**Files to modify/create**: +- `src/controllers/auth_controller.ts` — add `linkLinkedIn` handler (or modify `linkedinCallback`) +- `src/routes/auth_routes.ts` — register new route +- `src/utils/linkedin_oauth.ts` — reuse existing LinkedIn API helpers + +#### Task C: Connections Access Control for Guest Users + +**Purpose**: Enforce that guest users cannot access their connections until they upgrade their account (walkthrough Sections 10.2, 10.5, 10.6). + +**Options** (choose one): + +**Option 1 — Backend enforcement (recommended)**: +- Add middleware or guard check on ParticipantConnection query routes +- If `req.user.authProvider === 'guest'`, return `403 Forbidden` with message: `"Upgrade your account to access connections"` +- Affected routes: + - `GET /api/participantConnections/getByParticipantAndEvent` + - `GET /api/participantConnections/getByUserEmailAndEvent` +- Creating connections is still allowed (connections are made during gameplay), only reading is restricted + +**Option 2 — Frontend-only enforcement**: +- The user profile response already includes `authProvider` +- Frontend checks `authProvider === 'guest'` and shows the upgrade screen instead of connections +- No backend changes needed, but less secure (API still returns data if called directly) + +**Implementation** (Option 1): +- Add a helper function `requireUpgradedAccount` in `src/middleware/` or inline in the controller +- Check user's `authProvider` field from the database (or from JWT payload if added) +- Return 403 with a descriptive error message for guest users + +**Frontend Dependency**: Mobile app "Locked Connections" screen (walkthrough Section 10.5) routes to Account Upgrade Screen. + +--- + +## Phase 1: Event Model Updates & Status API (Critical Priority) + +These features are required for the mobile app to work without mock data. The mobile client depends on `gameType`, `eventImg`, and `currentState` enum values from the Event model, and polls for status transitions in the lobby. + +### 1.1 Add `gameType` and `eventImg` to Event Model ✅ COMPLETE + +**File**: `src/models/event_model.ts` + +**Changes**: +```typescript +gameType: { + type: String, + enum: ['Name Bingo'], + required: true +}, +eventImg: { + type: String, + required: false +} +``` + +**Why**: Mobile renders `gameType` and `eventImg` from event data. Without these fields, the mobile app uses hardcoded fallbacks. + +**Impact on existing endpoints**: +- `POST /api/events/createEvent` — accepts `gameType` (required) and `eventImg` (optional) in request body +- `GET /api/events/:eventId` and `GET /api/events/event/:joinCode` — automatically included in response + +**Frontend Dependency**: Mobile event cards display game type badge and event image. + +--- + +### 1.2 Add Event Status Enum to Event Model ✅ COMPLETE + +**File**: `src/models/event_model.ts` + +**Change**: +```typescript +// Before +currentState: { type: String, required: true } + +// After +currentState: { + type: String, + enum: ['Upcoming', 'In Progress', 'Completed'], + default: 'Upcoming', + required: true +} +``` + +**Important**: These enum values match the mobile app's `EventState` enum exactly (title case with spaces): +- `"Upcoming"` — event created but not started +- `"In Progress"` — event is live, bingo game active +- `"Completed"` — event has ended + +**Migration consideration**: Any existing events with free-form `currentState` values (e.g., `"pending"`, `"active"`) will need to be updated to match the new enum values. Check existing data before applying. + +--- + +### 1.3 Event Status Transition API ✅ COMPLETE + +**Endpoint**: `PUT /api/events/:eventId/status` + +**Request Body**: `{ "status": "In Progress" }` or `{ "status": "Completed" }` + +**Valid Transitions**: +- `Upcoming` → `In Progress` (host starts event) +- `In Progress` → `Completed` (host ends event) + +**Security**: +- Protected route (requires auth) +- Verifies `event.createdBy === req.user.userId` (host only) + +**Side Effects**: +- Emits Pusher `event-started` on channel `event-${eventId}` when transitioning to `In Progress` (payload: `{ status: 'In Progress' }`) +- Emits Pusher `event-ended` on channel `event-${eventId}` when transitioning to `Completed` (payload: `{ status: 'Completed' }`) + +**Implementation**: +- Handler in `src/controllers/event_controller.ts` (`updateEventStatus`) +- Route in `src/routes/event_routes.ts` +- Validates transition is allowed (rejects invalid transitions with 400) +- Returns updated event + +**Frontend Dependency**: Web dashboard Start/End buttons; mobile lobby polls for status change to transition into game. + +--- + +## Phase 2: Real-time Game Events (High Priority) + +### 2.1 Pusher Events for Game State + +| Event | Channel | Payload | Trigger | Status | +|-------|---------|---------|---------|--------| +| `event-started` | `event-${eventId}` | `{ status: 'In Progress' }` | Host starts event (Phase 1.3) | ✅ Complete | +| `event-ended` | `event-${eventId}` | `{ status: 'Completed' }` | Host ends event (Phase 1.3) | ✅ Complete | +| `bingo-achieved` | `event-${eventId}` | `{ participantId, name, type: 'line' \| 'blackout' }` | Player completes line/blackout (future, if server-side game state is added) | Planned | + +**Note**: `event-started` and `event-ended` were implemented as part of Phase 1.3. The `bingo-achieved` event is deferred until server-side game state tracking is implemented (Phase 6), since mobile currently handles win detection client-side. + +--- + +## Phase 3: User & Event Management (Medium Priority) + +### 3.1 User Profile APIs ✅ COMPLETE + +**GET Endpoint**: `GET /api/users/:userId` ✅ +- Returns user profile excluding `passwordHash` +- Protected route (requires auth) +- Located in `user_controller.ts` (`getUserById`) + +**PUT Endpoint**: `PUT /api/users/:userId` ✅ +- **Updatable Fields**: `name`, `email`, `password`, `bio`, `profilePhoto`, `socialLinks` +- **Security**: Protected, self-only (`req.user.userId === req.params.userId` → 403 otherwise) +- **Guest upgrade**: Setting a password upgrades `authProvider` from `'guest'` to `'local'` +- **Validation**: Email format + uniqueness, password >= 8 chars, name cannot be empty +- Located in `user_controller.ts` (`updateUser`), route in `user_route.ts` + +--- + +### 3.2 Leave Event API + +**Endpoint**: `POST /api/events/:eventId/leave` + +**Logic**: +1. Find participant for `req.user.userId` + `eventId` +2. Verify user is not the host (hosts must delete, not leave) +3. Remove participant from `Event.participantIds` +4. Delete `Participant` document +5. Remove event from `User.eventHistoryIds` +6. Emit Pusher `participant-left` event + +--- + +### 3.3 Delete/Cancel Event API + +**Endpoint**: `DELETE /api/events/:eventId` + +**Constraints**: +- Host only (`event.createdBy === req.user.userId`) +- Only when `status === 'Upcoming'` (before event starts) + +**Cascade**: +- Delete all `Participant` documents for this event +- Delete the `Bingo` document +- Remove event from all users' `eventHistoryIds` + +--- + +### 3.4 Event History API ✅ COMPLETE + +**Endpoint**: `GET /api/users/:userId/events` (protected) + +**Purpose**: MVP requirement from `feature_list.md` - "View Previous Events (static list of past events)" + +**Response**: +```json +{ + "success": true, + "events": [ + { + "_id": "665a...", + "name": "Tech Mixer", + "description": "Monthly networking event", + "joinCode": "12345678", + "startDate": "2025-02-01T18:00:00.000Z", + "endDate": "2025-02-01T21:00:00.000Z", + "currentState": "Completed", + "participantIds": [ + { "_id": "666b...", "name": "John Doe", "userId": "664f..." }, + { "_id": "666c...", "name": "Jane Smith", "userId": "664e..." } + ] + } + ] +} +``` + +**Implementation**: +- Populates `eventHistoryIds` from the User document with nested populate on `participantIds` +- Each event includes participant data (`name`, `userId`) to enable loading participant connections +- Located in `user_controller.ts` (`getUserEvents`) + +**Frontend Dependency**: Mobile app "Previous Events" tab (right navigation tab). + +--- + +### 3.5 Get Current Event API + +**Endpoint**: `GET /api/users/:userId/current-event` + +**Purpose**: MVP requirement - "View Current Event" feature + +**Response** (if user is in an active event): +```json +{ + "hasActiveEvent": true, + "event": { + "_id": "...", + "eventName": "Tech Mixer", + "status": "In Progress", + "joinCode": "ABC123", + "participantCount": 25, + "role": "participant" + } +} +``` + +**Response** (if no active event): +```json +{ + "hasActiveEvent": false, + "event": null +} +``` + +**Logic**: +1. Query `Participant` collection for user's participation +2. Join with `Event` collection +3. Filter for `status` in ['Upcoming', 'In Progress'] +4. Return most recent if multiple (edge case) + +**Security**: Protected route, user can only query their own current event + +**Frontend Dependency**: Mobile app needs this to determine if user should see event lobby or join screen. + +--- + +## Phase 4: Validation (Medium Priority) + +### 4.1 Zod Validation Schemas with Sanitization + +**File**: `src/validation/schemas.ts` + +**Schemas to Create**: +- `SignupSchema`, `LoginSchema` +- `CreateEventSchema`, `JoinEventSchema` +- `CreateBingoSchema` +- `UpdateProfileSchema` + +**Input Sanitization** (add to all string fields): +```typescript +import { z } from 'zod'; + +// Sanitization helper +const sanitizeString = (str: string) => str.trim(); + +// Example schema with sanitization +const SignupSchema = z.object({ + name: z.string().min(1).max(100).transform(sanitizeString), + email: z.string().email().transform(s => s.toLowerCase().trim()), + password: z.string().min(8).max(128) +}); +``` + +**Middleware**: `src/middleware/validate.ts` +```typescript +export const validate = (schema: ZodSchema) => (req, res, next) => { + const result = schema.safeParse(req.body); + if (!result.success) return res.status(400).json({ errors: result.error.issues }); + req.body = result.data; // Use sanitized data + next(); +}; +``` + +--- + +## Phase 5: Documentation Tasks ✅ COMPLETE + +All documentation has been created and is located in `shatter-backend/docs/`. + +### 5.1 API_REFERENCE.md ✅ COMPLETE +- Comprehensive endpoint documentation (1,164 lines) +- Covers all implemented endpoints with request/response examples, error codes, and auth requirements +- Located at `docs/API_REFERENCE.md` + +### 5.2 REALTIME_EVENTS_GUIDE.md ✅ COMPLETE +- Pusher setup, channel naming, event payloads, client-side examples +- Located at `docs/REALTIME_EVENTS_GUIDE.md` + +### 5.3 DATABASE_SCHEMA.md ✅ COMPLETE +- All collections with field definitions, indexes, relationships, and pre-save hooks +- Located at `docs/DATABASE_SCHEMA.md` + +### 5.4 EVENT_LIFECYCLE.md ✅ COMPLETE +- State diagram, transition rules, side effects, frontend integration notes +- Located at `docs/EVENT_LIFECYCLE.md` + +--- + +## Phase 6: Polish & Production Ready (Low Priority) + +These are P3 tasks that improve UX and performance but aren't blocking for MVP. Includes tasks deferred from earlier phases because mobile handles them client-side. + +### 6.1 Edit Bingo Square API + +**Endpoint**: Extend `POST /api/bingo/:bingoId/fill-cell` + +**Purpose**: Allow users to change already-filled cells (fix mistakes). + +**Logic**: +- If cell already filled, allow changing the assigned person +- Re-run line detection after change +- Could decrease `completedLines` if editing breaks a line +- Optional: Add event config `allowCellEditing: boolean` + +**Frontend Dependency**: Mobile app cell tap on filled cell shows edit option. + +--- + +### 6.2 Prevent Duplicate Person Assignments (Server-side) + +**Purpose**: Game rule validation (configurable per event). Mobile already handles this client-side for MVP. + +**Implementation**: +- Add to Bingo model: `allowDuplicateAssignments: boolean` (default: true) +- If false, validate `matchedParticipantId` not already used in another cell +- Return 400 error: "You've already assigned {name} to another square" + +--- + +### 6.3 Bingo Leaderboard API + +**Endpoint**: `GET /api/events/:eventId/bingo/leaderboard` + +**Purpose**: Show who completed lines/blackout first. Requires server-side game state (Phase 6.6). + +**Response**: +```json +{ + "leaderboard": [ + { + "rank": 1, + "participantId": "...", + "name": "Alice", + "linesCompleted": 3, + "blackoutAchieved": true, + "blackoutAt": "2024-01-15T14:30:00Z" + } + ] +} +``` + +**Logic**: +- Aggregate all `PlayerBingoState` for the event +- Sort by: `blackoutAchieved` (true first), then `blackoutAt` (earliest first), then `completedLines` (most first) + +**Frontend Dependency**: Optional leaderboard display during/after game. + +--- + +### 6.4 Database Indexes for Performance + +**Purpose**: Optimize queries for scale. + +**Indexes to Create**: + +| Collection | Index | Type | +|------------|-------|------| +| `events` | `joinCode` | unique | +| `events` | `currentState` | regular | +| `events` | `createdBy` | regular | +| `participants` | `(eventId, name)` | compound, unique (case-insensitive collation) | +| `participants` | `eventId` | regular | +| `users` | `email` | unique | +| `users` | `contactLink` | unique, sparse | +| `users` | `linkedinUrl` | unique, sparse | + +**Implementation**: Add to model definitions or create migration script. + +--- + +### 6.5 Participant Search API (Deferred from Phase 1) + +**Note**: Deferred because the mobile app gets participant names from the event's `participantIds` array (populated on `GET /api/events/:eventId`). A dedicated search endpoint is a nice-to-have for large events but not required for MVP. + +**Endpoint**: `GET /api/events/:eventId/participants/search` + +**Query Params**: `?name=` + +**Purpose**: Enable fuzzy name matching for the bingo cell-filling modal (see `bingo_walkthrough.md` Section 7.1). + +**Requirements**: +- Case-insensitive search on `Participant.name` +- Support partial matches (e.g., "joh" matches "John", "Johnny") +- Return max 10 results +- **Include profilePhoto in response** (per bingo_walkthrough 7.1) +- Response: `{ participants: [{ _id, name, profilePhoto }] }` + +**Implementation**: +- Create `src/controllers/participant_controller.ts` +- Create `src/routes/participant_routes.ts` +- Mount at `/api/participants` in `app.ts` +- Use MongoDB `$regex` with `'i'` flag for case-insensitive matching +- Join with User collection to get `profilePhoto` + +**Frontend Dependency**: Mobile app "Who did you find?" modal — currently uses in-memory filtering of participant list. + +--- + +### 6.6 PlayerBingoState Model (Deferred from Phase 1) + +**Note**: Deferred because mobile handles all bingo game state client-side via AsyncStorage for MVP. Server-side state tracking becomes important post-MVP for leaderboards, analytics, and cross-device sync. + +**File**: `src/models/player_bingo_state_model.ts` + +**Schema**: +```typescript +{ + eventId: ObjectId, // ref: Event + bingoId: string, // ref: Bingo + participantId: ObjectId, // ref: Participant (the player) + filledCells: [{ + row: number, + col: number, + matchedParticipantId: ObjectId | null, // null for free-text entries + matchedName: string, + filledAt: Date + }], + completedLines: number, + firstBingoAt: Date | null, + blackoutAt: Date | null, + isLocked: boolean +} +``` + +**Bingo Model Enhancements** (add to existing `bingo_model.ts`): +```typescript +{ + // Existing fields... + gridSize: { + type: Number, + enum: [3, 4, 5], + default: 5, + required: true + }, + prompts: [{ + text: String, // Full prompt text + shortText: String // Abbreviated version (e.g., "Has dog") + }], + allowFreeTextEntry: { + type: Boolean, + default: false // Allow typing names not in participant list + }, + allowDuplicateAssignments: { + type: Boolean, + default: true + } +} +``` + +**Indexes**: +- Compound unique index on `(eventId, participantId)` + +--- + +### 6.7 Cell Fill API (Deferred from Phase 1) + +**Note**: Deferred because mobile manages cell fills locally via AsyncStorage for MVP. + +**Endpoint**: `POST /api/bingo/:bingoId/fill-cell` + +**Request Body**: +```json +{ + "participantId": "player's participant ID", + "row": 0, + "col": 2, + "matchedParticipantId": "matched person's participant ID", + "matchedName": "John Doe" +} +``` + +**Logic**: +1. Validate player is in the event +2. If `matchedParticipantId` provided, validate matched participant exists in event +3. If `matchedParticipantId` is null, verify `allowFreeTextEntry` is true +4. Check cell not already filled +5. Update `PlayerBingoState.filledCells` +6. Run line detection (rows, columns, diagonals) +7. Check for blackout (all cells filled) +8. Record `firstBingoAt` on first line completion +9. Record `blackoutAt` on blackout +10. Emit Pusher event `bingo-achieved` if line/blackout + +--- + +### 6.8 Get Player Bingo State API (Deferred from Phase 1) + +**Note**: Deferred because mobile uses AsyncStorage for game state persistence for MVP. + +**Endpoint**: `GET /api/bingo/:bingoId/state/:participantId` + +**Response**: +```json +{ + "gridSize": 5, + "grid": [ + [{"text": "Has a dog", "shortText": "Has dog"}] + ], + "filledCells": [{ "row": 0, "col": 1, "matchedName": "John", "matchedParticipantId": "..." }], + "completedLines": 0, + "firstBingoAt": null, + "blackoutAt": null, + "isLocked": false, + "allowFreeTextEntry": false +} +``` + +--- + +### 6.9 Error Handling Middleware + +**File**: `src/middleware/error_handler.ts` + +**Purpose**: Consistent error responses across all endpoints. + +**Implementation**: +```typescript +export const errorHandler = (err, req, res, next) => { + console.error(err.stack); + + // Mongoose validation errors + if (err.name === 'ValidationError') { + return res.status(400).json({ + error: 'Validation failed', + details: Object.values(err.errors).map(e => e.message) + }); + } + + // Mongoose duplicate key + if (err.code === 11000) { + return res.status(409).json({ + error: 'Duplicate entry', + field: Object.keys(err.keyPattern)[0] + }); + } + + // JWT errors + if (err.name === 'JsonWebTokenError') { + return res.status(401).json({ error: 'Invalid token' }); + } + + // Default + res.status(500).json({ error: 'Internal server error' }); +}; +``` + +**Mount in `app.ts`**: `app.use(errorHandler)` after all routes. + +--- + +### 6.10 Rate Limiting Middleware + +**File**: `src/middleware/rate_limiter.ts` + +**Purpose**: Protect against brute force attacks and API abuse (feature_list.md "Wants") + +**Implementation**: +```typescript +import rateLimit from 'express-rate-limit'; + +// Strict limiter for auth endpoints +export const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 10, // 10 requests per window + message: { error: 'Too many login attempts, please try again later' }, + standardHeaders: true, + legacyHeaders: false +}); + +// General API limiter +export const apiLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + message: { error: 'Too many requests, please slow down' } +}); +``` + +**Installation**: +```bash +npm install express-rate-limit +npm install -D @types/express-rate-limit +``` + +--- + +### 6.11 Organizer Analytics Endpoints (Low Priority / Future) + +**Reference**: `feature_list.md` "Wants" section — "Organizer Analytics endpoints" + +**Purpose**: Provide organizers with event analytics such as attendance count, average engagement, and participation stats. + +**Potential Endpoints**: +- `GET /api/events/:eventId/analytics` — Returns event-level analytics (protected, host only) + - Participant count, bingo completion rates, average lines completed, blackout count + - Connection counts, most-connected participants + - Time-based metrics (average time to first bingo, event duration) + +**Implementation Notes**: +- Aggregate data from `Participant`, `PlayerBingoState`, and `ParticipantConnection` collections +- Consider caching analytics results for ended events (data won't change) +- Low priority — not blocking MVP + +**Frontend Dependency**: Web organizer dashboard "Event Summary Page" and analytics views. + +--- + +## Sprint Recommendations + +### Sprint 1: Event Model Updates & Status API ✅ COMPLETE +- ~~Task 1.1: Add `gameType` and `eventImg` to Event model~~ ✅ +- ~~Task 1.2: Add `currentState` enum (`Upcoming`, `In Progress`, `Completed`)~~ ✅ +- ~~Task 1.3: Event Status Transition API (`PUT /api/events/:eventId/status`)~~ ✅ + +### Sprint 2: Guest Account Upgrades & Real-time +- Task 0.3B: LinkedIn Account Linking for Guest Users +- Task 0.3C: Connections Access Control for Guests +- ~~Task 2.1: Pusher game events (`event-started`, `event-ended`)~~ ✅ Implemented as part of Phase 1.3 + +### Sprint 3: Event Management +- ~~Task 3.1: User Profile APIs (GET and PUT)~~ ✅ Complete +- Task 3.2: Leave Event API +- Task 3.3: Delete/Cancel Event API +- ~~Task 3.4: Event History API~~ ✅ Complete +- Task 3.5: Get Current Event API + +### Sprint 4: Validation +- Task 4.1: Zod Validation Schemas with Sanitization +- ~~Documentation tasks (5.1-5.4)~~ ✅ Complete + +### Sprint 5: Server-side Game State & Polish (if needed post-MVP) +- Task 6.5: Participant Search API +- Task 6.6: PlayerBingoState Model +- Task 6.7: Cell Fill API +- Task 6.8: Get Player Bingo State API +- Task 6.1: Edit Bingo Square +- Task 6.2: Prevent Duplicate Assignments (server-side) +- Task 6.3: Bingo Leaderboard +- Task 6.4: Database Indexes +- Task 6.9: Error Handling Middleware +- Task 6.10: Rate Limiting Middleware +- Task 6.11: Organizer Analytics Endpoints + +--- + +## Dependencies to Install + +```bash +# Authentication - Already installed: bcryptjs, jsonwebtoken, axios (for LinkedIn OAuth) +# No passport needed - LinkedIn OAuth uses custom implementation with axios + +# Validation +npm install zod + +# Rate Limiting +npm install express-rate-limit +npm install -D @types/express-rate-limit +``` + +--- + +## Data Model Summary + +### User Collection +```typescript +{ + _id: ObjectId, + name: string, + email?: string (unique, sparse index), // optional for guest users + passwordHash?: string (select: false), + linkedinId?: string (unique, sparse index), // LinkedIn subject ID + linkedinUrl?: string (unique, sparse index), + authProvider: 'local' | 'linkedin' | 'guest' (default: 'local'), + bio?: string, + profilePhoto?: string, + socialLinks?: { + linkedin?: string, + github?: string, + other?: string + }, + lastLogin?: Date, + passwordChangedAt?: Date, + eventHistoryIds: ObjectId[], + createdAt: Date, + updatedAt: Date +} +``` + +### AuthCode Collection (temporary, auto-expiring) +```typescript +{ + _id: ObjectId, + code: string (unique, indexed), + userId: ObjectId (ref: User), + createdAt: Date (TTL: 60 seconds) +} +``` + +### Event Collection +```typescript +{ + _id: ObjectId, + name: string, + description: string, + createdBy: ObjectId (ref: User), + joinCode: string (unique), + currentState: 'Upcoming' | 'In Progress' | 'Completed' (default: 'Upcoming'), + gameType: 'Name Bingo' (required), + eventImg?: string, + startDate: Date, + endDate: Date, + maxParticipant: number, + participantIds: ObjectId[] (ref: Participant), + createdAt: Date, + updatedAt: Date +} +``` + +### Participant Collection +```typescript +{ + _id: ObjectId, + eventId: ObjectId (ref: Event, required), + userId: ObjectId | null (ref: User, default: null), // nullable, not required + name: string (required) +} +// Index: (eventId, name) compound unique with case-insensitive collation +// Note: duplicate names get an automatic #XXX suffix (e.g., "John#472") instead of being rejected +// Note: no role or joinedAt fields in current model +``` + +### Bingo Collection +```typescript +{ + _id: string (auto-generated, e.g. "bingo_xxxxxxxx"), + _eventId: ObjectId (ref: Event, required), + description?: string, + grid?: string[][] (2D string array) +} +// Note: gridSize, prompts, allowFreeTextEntry, allowDuplicateAssignments are planned for Phase 6 +``` + +### ParticipantConnection Collection ✅ IMPLEMENTED +```typescript +{ + _id: string (auto-generated, e.g. "participantConnection_xxxxxxxx"), + _eventId: ObjectId (ref: Event, required), + primaryParticipantId: ObjectId (ref: Participant, required), + secondaryParticipantId: ObjectId (ref: Participant, required), + description?: string +} +// Duplicate prevention: checked via query on (_eventId, primaryParticipantId, secondaryParticipantId) +``` + +### PlayerBingoState Collection (Planned — Phase 6) +```typescript +{ + _id: ObjectId, + eventId: ObjectId (ref: Event), + bingoId: ObjectId (ref: Bingo), + participantId: ObjectId (ref: Participant), + filledCells: [{ + row: number, + col: number, + matchedParticipantId: ObjectId | null, + matchedName: string, + filledAt: Date + }], + completedLines: number, + firstBingoAt: Date | null, + blackoutAt: Date | null, + isLocked: boolean +} +// Index: (eventId, participantId) compound unique +``` + +--- + +## API Summary + +### Authentication +``` +POST /api/auth/signup - Email/password OR contact link signup +POST /api/auth/login - Email/password login +GET /api/auth/linkedin - Initiate LinkedIn OAuth ✅ +GET /api/auth/linkedin/callback - LinkedIn OAuth callback ✅ +POST /api/auth/exchange - Exchange auth code for JWT ✅ +POST /api/auth/linkedin/link - Link LinkedIn to existing guest account (protected, guest only) +GET /api/users/me - Get current user (protected) +``` + +### Users +``` +GET /api/users/:userId - Get user profile ✅ +PUT /api/users/:userId - Update user profile (protected, self only) ✅ +GET /api/users/:userId/events - Get user's event history (protected) ✅ +GET /api/users/:userId/current-event - Get user's active event (protected) +``` + +### Events +``` +POST /api/events - Create event (protected) +POST /api/events/join - Join event with joinCode (protected) +GET /api/events/:eventId - Get event details +PUT /api/events/:eventId/status - Update event status (protected, host only) +POST /api/events/:eventId/leave - Leave event (protected) +DELETE /api/events/:eventId - Cancel event (protected, host only, Upcoming only) +GET /api/events/:eventId/participants/search - Search participants (protected, Phase 6) +``` + +### Participant Connections ✅ IMPLEMENTED +``` +POST /api/participantConnections - Create connection by participant IDs (protected) ✅ +POST /api/participantConnections/by-emails - Create connection by user emails (protected) ✅ +DELETE /api/participantConnections/delete - Delete connection (protected) ✅ +GET /api/participantConnections/getByParticipantAndEvent - Get connections by participant & event (protected) ✅ +GET /api/participantConnections/getByUserEmailAndEvent - Get connections by user email & event (protected) ✅ +``` + +### Name Bingo (Phase 6 — server-side game state) +``` +GET /api/bingo/:bingoId/state/:participantId - Get player's board state (protected) +POST /api/bingo/:bingoId/fill-cell - Fill a cell (protected) +GET /api/events/:eventId/bingo/leaderboard - Get leaderboard (protected) +``` diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 16b7d53..fcc2db3 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@google/genai": "^1.41.0", + "@modelcontextprotocol/sdk": "^1.27.1", "axios": "^1.13.5", "bcryptjs": "^3.0.3", "cors": "^2.8.5", @@ -32,451 +33,22 @@ "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", - "tsx": "^4.21.0", + "ts-node-dev": "^2.0.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@eslint-community/eslint-utils": { @@ -659,6 +231,18 @@ } } }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -728,6 +312,96 @@ "node": ">=12" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.1.tgz", + "integrity": "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", @@ -817,6 +491,34 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -988,6 +690,20 @@ "socket.io": "*" } }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1342,6 +1058,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1368,6 +1097,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1395,6 +1163,40 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1472,6 +1274,19 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -1507,6 +1322,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bson": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", @@ -1522,6 +1350,13 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1587,6 +1422,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1681,6 +1554,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1746,6 +1626,16 @@ "node": ">= 0.8" } }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", @@ -1772,6 +1662,16 @@ "node": ">= 0.4" } }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1947,48 +1847,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2180,6 +2038,27 @@ "node": ">=6" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/express": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", @@ -2223,6 +2102,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2233,7 +2130,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -2250,6 +2146,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2304,6 +2216,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2466,6 +2391,13 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2556,19 +2488,6 @@ "node": ">= 0.4" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -2724,7 +2643,16 @@ "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" } }, "node_modules/http-errors": { @@ -2813,12 +2741,33 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2838,6 +2787,35 @@ "is-base64": "bin/is-base64" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2870,6 +2848,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -2880,8 +2868,9 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -2920,6 +2909,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2956,6 +2954,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -3116,6 +3120,13 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3190,6 +3201,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -3199,6 +3220,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mongodb-connection-string-url": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", @@ -3235,8 +3269,9 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "debug": "4" }, @@ -3248,8 +3283,9 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", - "extraneous": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", @@ -3264,8 +3300,9 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", - "extraneous": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "gaxios": "^5.0.0", "json-bigint": "^1.0.0" @@ -3278,8 +3315,9 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -3338,8 +3376,9 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -3359,22 +3398,25 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "extraneous": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/mongoose/node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "extraneous": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "optional": true, + "peer": true }, "node_modules/mongoose/node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "extraneous": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -3461,6 +3503,16 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3604,6 +3656,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3613,6 +3675,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -3652,6 +3721,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3812,24 +3890,70 @@ "node": ">= 0.10" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, "engines": { - "node": ">=4" + "node": ">=8.10.0" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, "node_modules/retry": { @@ -4177,6 +4301,27 @@ "node": ">= 0.6" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -4291,6 +4436,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -4317,6 +4472,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4334,6 +4502,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4355,6 +4536,16 @@ "node": ">=18" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -4368,24 +4559,142 @@ "typescript": ">=4.8.4" } }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" }, "bin": { - "tsx": "dist/cli.mjs" + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=0.8.0" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ts-node-dev/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/tweetnacl": { @@ -4490,6 +4799,13 @@ "punycode": "^2.1.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4670,6 +4986,26 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/shatter-backend/package.json b/shatter-backend/package.json index 4bc301b..b4240b0 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -2,9 +2,10 @@ "name": "shatter-backend", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "tsx watch src/server.ts", + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", "build": "tsc", "start": "node dist/server.js" }, @@ -14,6 +15,7 @@ "description": "", "dependencies": { "@google/genai": "^1.41.0", + "@modelcontextprotocol/sdk": "^1.27.1", "axios": "^1.13.5", "bcryptjs": "^3.0.3", "cors": "^2.8.5", @@ -36,7 +38,7 @@ "eslint": "^9.38.0", "globals": "^16.4.0", "jiti": "^2.6.1", - "tsx": "^4.21.0", + "ts-node-dev": "^2.0.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.2" } diff --git a/shatter-backend/src/ai/prompts/bingo_short_questions.txt b/shatter-backend/src/ai/prompts/bingo_short_questions.txt new file mode 100644 index 0000000..8b12320 --- /dev/null +++ b/shatter-backend/src/ai/prompts/bingo_short_questions.txt @@ -0,0 +1,2 @@ +You will generate the bingo grid with shortened version of the questions while maintaining the same structure. +Each entry should be turned into a max 3 word version of the question that describes what the question is about. diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 7f5f7ea..1f9600b 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,11 +1,11 @@ import express from "express"; import cors from "cors"; -import userRoutes from './routes/user_route'; -import authRoutes from './routes/auth_routes'; -import eventRoutes from './routes/event_routes'; -import bingoRoutes from './routes/bingo_routes'; -import participantConnectionRoutes from "./routes/participant_connections_routes"; +import userRoutes from './routes/user_route.js'; +import authRoutes from './routes/auth_routes.js'; +import eventRoutes from './routes/event_routes.js'; +import bingoRoutes from './routes/bingo_routes.js'; +import participantConnectionRoutes from "./routes/participant_connections_routes.js"; const app = express(); diff --git a/shatter-backend/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 7d23c0c..9303990 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -1,11 +1,11 @@ import crypto from 'crypto'; import jwt from 'jsonwebtoken'; import { Request, Response } from 'express'; -import { User } from '../models/user_model'; -import { AuthCode } from '../models/auth_code_model'; -import { hashPassword, comparePassword } from '../utils/password_hash'; -import { generateToken } from '../utils/jwt_utils'; -import { getLinkedInAuthUrl, getLinkedInAccessToken, getLinkedInProfile } from '../utils/linkedin_oauth'; +import { User } from '../models/user_model.js'; +import { AuthCode } from '../models/auth_code_model.js'; +import { hashPassword, comparePassword } from '../utils/password_hash.js'; +import { generateToken } from '../utils/jwt_utils.js'; +import { getLinkedInAuthUrl, getLinkedInAccessToken, getLinkedInProfile } from '../utils/linkedin_oauth.js'; const JWT_SECRET = process.env.JWT_SECRET || ''; @@ -57,8 +57,13 @@ export const signup = async (req: Request, res: Response) => { // check if email already exists const existingUser = await User.findOne({ email: normalizedEmail }).lean(); if (existingUser) { + if (existingUser.authProvider === 'linkedin') { + return res.status(409).json({ + error: 'This email is associated with a LinkedIn account. Please log in with LinkedIn.', + }); + } return res.status(409).json({ - error: 'Email already exists' + error: 'Email already exists', }); } diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 70c16f2..f2536c3 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -1,9 +1,9 @@ -// controllers/bingo_controller.ts +// controllers/bingo_controller.js import { Request, Response } from "express"; import { Types } from "mongoose"; -import { Bingo } from "../models/bingo_model"; -import { Event } from "../models/event_model"; -import { Prompt } from "../ai/prompt_builder"; +import { Bingo } from "../models/bingo_model.js"; +import { Event } from "../models/event_model.js"; +import { Prompt } from "../ai/prompt_builder.js"; import { GoogleGenAI } from "@google/genai"; import { z } from "zod"; @@ -13,7 +13,6 @@ import "dotenv/config"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { Console } from "node:console"; /** * POST /api/bingo @@ -46,18 +45,24 @@ export async function createBingo(req: Request, res: Response) { } if (grid !== undefined) { - const is2DStringArray = + const isValidGrid = Array.isArray(grid) && grid.every( (row: any) => Array.isArray(row) && - row.every((cell: any) => typeof cell === "string") + row.every( + (cell: any) => + cell && + typeof cell === "object" && + typeof cell.question === "string" && + typeof cell.shortQuestion === "string" + ) ); - if (!is2DStringArray) { + if (!isValidGrid) { return res.status(400).json({ success: false, - msg: "grid must be a 2D array of strings", + msg: "grid must be a 2D array of { question: string, shortQuestion: string }", }); } } @@ -98,11 +103,7 @@ export async function getBingo(req: Request, res: Response) { }); } - let bingo = await Bingo.findById(eventId); - - if (!bingo && Types.ObjectId.isValid(eventId)) { - bingo = await Bingo.findOne({ _eventId: eventId }); - } + let bingo = await Bingo.findOne({ _eventId: eventId }); if (!bingo) { return res.status(404).json({ @@ -149,18 +150,24 @@ export async function updateBingo(req: Request, res: Response) { } if (grid !== undefined) { - const is2DStringArray = + const isValidGrid = Array.isArray(grid) && grid.every( (row: any) => Array.isArray(row) && - row.every((cell: any) => typeof cell === "string") + row.every( + (cell: any) => + cell && + typeof cell === "object" && + typeof cell.question === "string" && + typeof cell.shortQuestion === "string" + ) ); - if (!is2DStringArray) { + if (!isValidGrid) { return res.status(400).json({ success: false, - msg: "grid must be a 2D array of strings", + msg: "grid must be a 2D array of { question: string, shortQuestion: string }", }); } @@ -198,8 +205,6 @@ export async function updateBingo(req: Request, res: Response) { function makeEmptyGrid(rows: number, cols: number): Record { - console.log(`Creating fallback grid ${rows}x${cols}`); - const grid: Record = {}; for (let r = 1; r <= rows; r++) { @@ -226,7 +231,6 @@ function buildSchema(rows: number, cols: number): z.ZodObject = {}; @@ -244,12 +248,13 @@ function buildShapeExample(rows: number, cols: number): string { * Calls Gemini to generate a bingo grid */ async function generateBingoGrid(n_rows: number, n_cols: number, context: string): Promise> { - const schema = buildSchema(n_rows, n_cols); const schemaJson = z.toJSONSchema(schema); const example = buildShapeExample(n_rows, n_cols); + const { GoogleGenAI } = await import("@google/genai"); + const basePrompt_structure = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: ${example} Rules: @@ -267,9 +272,6 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string const aiPrompt = new Prompt([basePrompt_structure, userContext, aiInstruction]) aiPrompt.generatePrompt(); const prompt = aiPrompt.getPrompt(); - console.log("----------- Final Prompt -----------"); - console.log(prompt); - console.log("------------------------------------"); try { @@ -283,16 +285,68 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string }, }); - console.log("RAW RESPONSE:"); - console.log(response.text); + if (!response.text) { + throw new Error("Gemini returned empty response"); + } + + const parsed = JSON.parse(response.text); + const validated: Record = schema.parse(parsed); + return validated; + + } catch (error) { + console.error("Generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) + } +} + +async function generateBingoGrid_shortVersions(n_rows: number, n_cols: number, original_bingo_questions: string): Promise> { + + const schema = buildSchema(n_rows, n_cols); + const schemaJson = z.toJSONSchema(schema); + const example = buildShapeExample(n_rows, n_cols); + + const basePrompt_structure = `Generate a ${n_rows}x${n_cols} bingo board. Return JSON exactly matching this structure: + ${example} + Rules: + - Keys must be row1, row2, row3, etc. + - Each row must contain ${n_cols} strings. + Return ONLY valid JSON. + + You will be provided with additional information about what to put inside the bingo squares. + `.trim(); + const original_bingo_questions_context = `These are the original bingo questions:\n${original_bingo_questions}`; + + const promptPath = path.resolve(__dirname, "../ai/prompts/bingo_short_questions.txt"); + const aiInstruction = fs.readFileSync(promptPath, "utf-8"); + + const aiPrompt = new Prompt([basePrompt_structure, original_bingo_questions_context, aiInstruction]) + aiPrompt.generatePrompt(); + const prompt = aiPrompt.getPrompt(); + + + try { + + const response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: prompt, + config: { + responseMimeType: "application/json", + responseJsonSchema: schemaJson, + temperature: 0.7, + }, + }); + + if (!response.text) { + throw new Error("Gemini returned empty response"); + } - const parsed = JSON.parse(response.text!); + const parsed = JSON.parse(response.text); const validated: Record = schema.parse(parsed); return validated; } catch (error) { - console.error("Generation failed:", error); - return makeEmptyGrid(n_rows, n_cols) + console.error("Short version generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) } } @@ -315,6 +369,15 @@ function process_ai_result(ai_result: Record) { return grid; } +function combine2DArrays(arr1: string[][], arr2: string[][]): { question: string; shortQuestion: string }[][] { + return arr1.map((row, i) => + row.map((val, j) => ({ + question: val, + shortQuestion: arr2[i]?.[j] || val, + })) + ); +} + /** * POST /api/bingo/generate * @@ -352,8 +415,11 @@ export async function generateBingo(req: Request, res: Response) { }); } - const aiResult = await generateBingoGrid(n_rows, n_cols, context); - const bingo_grid: string[][] = process_ai_result(aiResult); + const bingo_questions = await generateBingoGrid(n_rows, n_cols, context); + const bingo_short_versions = await generateBingoGrid_shortVersions(n_rows, n_cols, JSON.stringify(bingo_questions)); + const bingo_grid_questions: string[][] = process_ai_result(bingo_questions); + const bingo_grid_short_versions: string[][] = process_ai_result(bingo_short_versions); + const bingo_grid = combine2DArrays(bingo_grid_questions, bingo_grid_short_versions); return res.status(200).json({ status: true, diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index c2c0c91..4a7401a 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -1,15 +1,49 @@ import { Request, Response } from "express"; -import { Event } from "../models/event_model"; -import { pusher } from "../utils/pusher_websocket"; +import { Event } from "../models/event_model.js"; +import { pusher } from "../utils/pusher_websocket.js"; -import "../models/participant_model"; +import "../models/participant_model.js"; -import { generateJoinCode } from "../utils/event_utils"; -import { generateToken } from "../utils/jwt_utils"; -import { Participant } from "../models/participant_model"; -import { User } from "../models/user_model"; +import { generateJoinCode } from "../utils/event_utils.js"; +import { generateToken } from "../utils/jwt_utils.js"; +import { Participant, IParticipant } from "../models/participant_model.js"; +import { User } from "../models/user_model.js"; import { Types } from "mongoose"; +/** + * Create a participant with automatic name suffix on collision. + * If the name already exists in the event, retries with a random #XXX suffix. + */ +async function createParticipantWithRetry( + userId: Types.ObjectId | null, + name: string, + eventId: string, + maxRetries: number = 5 +): Promise<{ participant: IParticipant; finalName: string }> { + let finalName = name; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const participant = await Participant.create({ + userId, + name: finalName, + eventId, + }); + return { participant, finalName }; + } catch (e: any) { + if (e.code === 11000 && e.keyPattern?.name && e.keyPattern?.eventId) { + const suffix = String(Math.floor(Math.random() * 999) + 1).padStart( + 3, + "0" + ); + finalName = `${name}#${suffix}`; + continue; + } + throw e; + } + } + throw { code: 11000, keyPattern: { name: 1, eventId: 1 } }; +} + /** * POST /api/events/createEvent * Create a new event @@ -136,7 +170,8 @@ export async function getEventByJoinCode(req: Request, res: Response) { export async function joinEventAsUser(req: Request, res: Response) { try { const { name, userId } = req.body; - const { eventId } = req.params; + const rawEventId = req.params.eventId; + const eventId = Array.isArray(rawEventId) ? rawEventId[0] : rawEventId; if (!userId || !name || !eventId) return res.status(400).json({ @@ -159,22 +194,22 @@ export async function joinEventAsUser(req: Request, res: Response) { if (event.participantIds.length >= event.maxParticipant) return res.status(400).json({ success: false, msg: "Event is full" }); - let participant = await Participant.findOne({ + const existingParticipant = await Participant.findOne({ userId, eventId, }); - if (participant) { + if (existingParticipant) { return res .status(409) .json({ success: false, msg: "User already joined" }); } - participant = await Participant.create({ + const { participant, finalName } = await createParticipantWithRetry( userId, name, eventId, - }); + ); const participantId = participant._id as Types.ObjectId; @@ -196,14 +231,14 @@ export async function joinEventAsUser(req: Request, res: Response) { ); console.log("Room socket:", eventId); - console.log("Participant data:", { participantId, name }); + console.log("Participant data:", { participantId, name: finalName }); await pusher.trigger( `event-${eventId}`, // channel (room) "participant-joined", // event name { participantId, - name, + name: finalName, }, ); @@ -213,12 +248,6 @@ export async function joinEventAsUser(req: Request, res: Response) { }); } catch (e: any) { if (e.code === 11000) { - if (e.keyPattern?.name && e.keyPattern?.eventId) { - return res.status(409).json({ - success: false, - msg: "This name is already taken in this event", - }); - } if (e.keyPattern?.email) { return res.status(409).json({ success: false, @@ -244,8 +273,15 @@ export async function joinEventAsUser(req: Request, res: Response) { */ export async function joinEventAsGuest(req: Request, res: Response) { try { - const { name } = req.body; - const { eventId } = req.params; + const { name, email, socialLinks, organization, title } = req.body as { + name?: string; + email?: string; + socialLinks?: { linkedin?: string; github?: string; other?: string }; + organization?: string; + title?: string; + }; + const rawEventId = req.params.eventId; + const eventId = Array.isArray(rawEventId) ? rawEventId[0] : rawEventId; if (!name || !eventId) { return res.status(400).json({ @@ -254,6 +290,31 @@ export async function joinEventAsGuest(req: Request, res: Response) { }); } + // Require at least one contact method or organization + const hasEmail = email && email.trim(); + const hasSocialLink = socialLinks && ( + socialLinks.linkedin?.trim() || + socialLinks.github?.trim() || + socialLinks.other?.trim() + ); + const hasOrganization = organization && organization.trim(); + + if (!hasEmail && !hasSocialLink && !hasOrganization) { + return res.status(400).json({ + success: false, + msg: "At least one contact method (email or social link) or organization is required", + }); + } + + // Validate email format if provided + const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; + if (hasEmail && !EMAIL_REGEX.test(email.toLowerCase().trim())) { + return res.status(400).json({ + success: false, + msg: "Invalid email format", + }); + } + const event = await Event.findById(eventId); if (!event) { return res.status(404).json({ success: false, msg: "Event not found" }); @@ -267,17 +328,26 @@ export async function joinEventAsGuest(req: Request, res: Response) { const user = await User.create({ name, authProvider: 'guest', + ...(hasEmail && { email: email.toLowerCase().trim() }), + ...(hasSocialLink && { socialLinks }), + ...(hasOrganization && { organization: organization.trim() }), + ...(title && title.trim() && { title: title.trim() }), }); const userId = user._id as Types.ObjectId; const token = generateToken(userId.toString()); - // Create participant linked to the new user - const participant = await Participant.create({ + // Create participant linked to the new user, with automatic #XXX suffix on name collision + const { participant, finalName } = await createParticipantWithRetry( userId, name, eventId, - }); + ); + + // Update guest user's name to match the suffixed participant name + if (finalName !== name) { + await User.updateOne({ _id: userId }, { name: finalName }); + } const participantId = participant._id as Types.ObjectId; @@ -293,14 +363,14 @@ export async function joinEventAsGuest(req: Request, res: Response) { // Emit socket console.log("Room socket:", eventId); - console.log("Participant data:", { participantId, name }); + console.log("Participant data:", { participantId, name: finalName }); await pusher.trigger( `event-${eventId}`, // channel (room) "participant-joined", // event name { participantId, - name, + name: finalName, }, ); @@ -312,12 +382,6 @@ export async function joinEventAsGuest(req: Request, res: Response) { }); } catch (e: any) { if (e.code === 11000) { - if (e.keyPattern?.name && e.keyPattern?.eventId) { - return res.status(409).json({ - success: false, - msg: "This name is already taken in this event", - }); - } if (e.keyPattern?.email) { return res.status(409).json({ success: false, diff --git a/shatter-backend/src/controllers/participant_connections_controller.ts b/shatter-backend/src/controllers/participant_connections_controller.ts index e66874e..d2ee3e6 100644 --- a/shatter-backend/src/controllers/participant_connections_controller.ts +++ b/shatter-backend/src/controllers/participant_connections_controller.ts @@ -1,12 +1,12 @@ -// controllers/participant_connections_controller.ts +// controllers/participant_connections_controller.js import { Request, Response } from "express"; import { Types } from "mongoose"; -import { check_req_fields } from "../utils/requests_utils"; -import { User } from "../models/user_model"; -import { Participant } from "../models/participant_model"; -import { ParticipantConnection } from "../models/participant_connection_model"; +import { check_req_fields } from "../utils/requests_utils.js"; +import { User } from "../models/user_model.js"; +import { Participant } from "../models/participant_model.js"; +import { ParticipantConnection } from "../models/participant_connection_model.js"; /** * POST /api/participantConnections diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 55e745b..2b0796c 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; -import { User } from "../models/user_model"; -import { hashPassword } from "../utils/password_hash"; +import { User } from "../models/user_model.js"; +import "../models/participant_model.js"; +import { hashPassword } from "../utils/password_hash.js"; const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; @@ -76,7 +77,14 @@ export const getUserEvents = async (req: Request, res: Response) => { } const user = await User.findById(userId) - .populate("eventHistoryIds", "name description joinCode startDate endDate currentState") + .populate({ + path: "eventHistoryIds", + select: "name description joinCode startDate endDate currentState participantIds", + populate: { + path: "participantIds", + select: "name userId", + }, + }) .select("eventHistoryIds"); if (!user) { @@ -106,13 +114,15 @@ export const updateUser = async (req: Request, res: Response) => { return res.status(403).json({ success: false, error: "You can only update your own profile" }); } - const { name, email, password, bio, profilePhoto, socialLinks } = req.body as { + const { name, email, password, bio, profilePhoto, socialLinks, organization, title } = req.body as { name?: string; email?: string; password?: string; bio?: string; profilePhoto?: string; socialLinks?: { linkedin?: string; github?: string; other?: string }; + organization?: string; + title?: string; }; const updateFields: Record = {}; @@ -153,6 +163,8 @@ export const updateUser = async (req: Request, res: Response) => { if (bio !== undefined) updateFields.bio = bio; if (profilePhoto !== undefined) updateFields.profilePhoto = profilePhoto; if (socialLinks !== undefined) updateFields.socialLinks = socialLinks; + if (organization !== undefined) updateFields.organization = organization; + if (title !== undefined) updateFields.title = title; if (Object.keys(updateFields).length === 0) { return res.status(400).json({ success: false, error: "No fields to update" }); diff --git a/shatter-backend/src/middleware/auth_middleware.ts b/shatter-backend/src/middleware/auth_middleware.ts index 30c79b7..1133f91 100644 --- a/shatter-backend/src/middleware/auth_middleware.ts +++ b/shatter-backend/src/middleware/auth_middleware.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { Request, Response, NextFunction } from 'express'; -import { verifyToken } from '../utils/jwt_utils'; +import { verifyToken } from '../utils/jwt_utils.js'; /** * Extend Express Request type to include user property diff --git a/shatter-backend/src/models/bingo_model.ts b/shatter-backend/src/models/bingo_model.ts index 9f5dd3b..9fda83e 100644 --- a/shatter-backend/src/models/bingo_model.ts +++ b/shatter-backend/src/models/bingo_model.ts @@ -1,10 +1,15 @@ import { Schema, model, Types, HydratedDocument } from "mongoose"; +export interface BingoTile { + question: string; + shortQuestion: string; +} + export interface IBingo { _id: string; _eventId: Types.ObjectId; description?: string; - grid?: string[][]; + grid?: BingoTile[][]; } export type BingoDocument = HydratedDocument; @@ -18,7 +23,12 @@ const bingoSchema = new Schema( required: true, }, description: { type: String }, - grid: { type: [[String]] }, + grid: { + type: [[{ + question: { type: String, required: true }, + shortQuestion: { type: String, required: true }, + }]], + }, }, { versionKey: false, diff --git a/shatter-backend/src/models/event_model.ts b/shatter-backend/src/models/event_model.ts index 28aec64..380c800 100644 --- a/shatter-backend/src/models/event_model.ts +++ b/shatter-backend/src/models/event_model.ts @@ -1,7 +1,7 @@ import mongoose, { Schema, model, Document, Types } from "mongoose"; -import { User } from "../models/user_model"; +import { User } from "../models/user_model.js"; -import { IParticipant } from "./participant_model"; +import { IParticipant } from "./participant_model.js"; export interface IEvent extends Document { name: string; diff --git a/shatter-backend/src/models/user_model.ts b/shatter-backend/src/models/user_model.ts index 78e76c9..3d4bb1c 100644 --- a/shatter-backend/src/models/user_model.ts +++ b/shatter-backend/src/models/user_model.ts @@ -12,6 +12,8 @@ export interface IUser { passwordHash?: string; linkedinId?: string; linkedinUrl?: string; + organization?: string; + title?: string; bio?: string; profilePhoto?: string; socialLinks?: { @@ -65,6 +67,14 @@ const UserSchema = new Schema( unique: true, sparse: true, }, + organization: { + type: String, + trim: true, + }, + title: { + type: String, + trim: true, + }, bio: { type: String, trim: true, diff --git a/shatter-backend/src/routes/auth_routes.ts b/shatter-backend/src/routes/auth_routes.ts index e1487fc..736250c 100644 --- a/shatter-backend/src/routes/auth_routes.ts +++ b/shatter-backend/src/routes/auth_routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { signup, login, linkedinAuth, linkedinCallback, exchangeAuthCode } from '../controllers/auth_controller'; +import { signup, login, linkedinAuth, linkedinCallback, exchangeAuthCode } from '../controllers/auth_controller.js'; const router = Router(); diff --git a/shatter-backend/src/routes/bingo_routes.ts b/shatter-backend/src/routes/bingo_routes.ts index 1ed430b..a811f78 100644 --- a/shatter-backend/src/routes/bingo_routes.ts +++ b/shatter-backend/src/routes/bingo_routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; -import { createBingo, getBingo, updateBingo, generateBingo} from '../controllers/bingo_controller'; -import { authMiddleware } from '../middleware/auth_middleware'; +import { createBingo, getBingo, updateBingo, generateBingo} from '../controllers/bingo_controller.js'; +import { authMiddleware } from '../middleware/auth_middleware.js'; const router = Router(); diff --git a/shatter-backend/src/routes/event_routes.ts b/shatter-backend/src/routes/event_routes.ts index 03e2bf9..db982e5 100644 --- a/shatter-backend/src/routes/event_routes.ts +++ b/shatter-backend/src/routes/event_routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express'; -import { createEvent, getEventByJoinCode, getEventById, joinEventAsUser, joinEventAsGuest, getEventsByUserId, updateEventStatus } from '../controllers/event_controller'; -import { authMiddleware } from '../middleware/auth_middleware'; +import { createEvent, getEventByJoinCode, getEventById, joinEventAsUser, joinEventAsGuest, getEventsByUserId, updateEventStatus } from '../controllers/event_controller.js'; +import { authMiddleware } from '../middleware/auth_middleware.js'; const router = Router(); diff --git a/shatter-backend/src/routes/participant_connections_routes.ts b/shatter-backend/src/routes/participant_connections_routes.ts index 8bb6dc8..c05fe2c 100644 --- a/shatter-backend/src/routes/participant_connections_routes.ts +++ b/shatter-backend/src/routes/participant_connections_routes.ts @@ -1,4 +1,4 @@ -// routes/participant_connections_routes.ts +// routes/participant_connections_routes.js import { Router } from "express"; import { @@ -8,8 +8,8 @@ import { getConnectedUsersInfo, getConnectionsByParticipantAndEvent, getConnectionsByUserEmailAndEvent, -} from "../controllers/participant_connections_controller"; -import { authMiddleware } from "../middleware/auth_middleware"; +} from "../controllers/participant_connections_controller.js"; +import { authMiddleware } from "../middleware/auth_middleware.js"; const router = Router(); diff --git a/shatter-backend/src/routes/user_route.ts b/shatter-backend/src/routes/user_route.ts index 4d7688a..3989ce2 100644 --- a/shatter-backend/src/routes/user_route.ts +++ b/shatter-backend/src/routes/user_route.ts @@ -1,7 +1,7 @@ import { Router, Request, Response } from 'express'; -import { getUsers, createUser, getUserById, getUserEvents, updateUser } from '../controllers/user_controller'; -import { authMiddleware } from '../middleware/auth_middleware'; -import { User } from '../models/user_model'; +import { getUsers, createUser, getUserById, getUserEvents, updateUser } from '../controllers/user_controller.js'; +import { authMiddleware } from '../middleware/auth_middleware.js'; +import { User } from '../models/user_model.js'; const router = Router(); diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index e209977..6a97ce1 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,6 +1,6 @@ import "dotenv/config"; import mongoose from "mongoose"; -import app from "./app"; +import app from "./app.js"; const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; const MONGODB_URI = process.env.MONGO_URI; diff --git a/shatter-backend/src/utils/test_jwt.ts b/shatter-backend/src/utils/test_jwt.ts index edd99e8..7cc54db 100644 --- a/shatter-backend/src/utils/test_jwt.ts +++ b/shatter-backend/src/utils/test_jwt.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; // Load environment variables dotenv.config(); -import { generateToken, verifyToken } from './jwt_utils'; +import { generateToken, verifyToken } from './jwt_utils.js'; function testJWT() { diff --git a/shatter-backend/src/utils/test_password.ts b/shatter-backend/src/utils/test_password.ts index e8ff743..0cbe1fb 100644 --- a/shatter-backend/src/utils/test_password.ts +++ b/shatter-backend/src/utils/test_password.ts @@ -1,4 +1,4 @@ -import { hashPassword, comparePassword } from './password_hash'; +import { hashPassword, comparePassword } from './password_hash.js'; async function testPasswordHashing() { console.log('🧪 Testing Password Hashing...\n'); diff --git a/shatter-backend/tsconfig.json b/shatter-backend/tsconfig.json index 532b6dc..0dee230 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -1,17 +1,16 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["node"], - "typeRoots": ["./src/types", "./node_modules/@types"], - "skipLibCheck": true + "lib": ["ES2020", "DOM"], + "skipLibCheck": true, + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"], "exclude": ["node_modules", "dist"]