From c730a856de4e386ae1f07e9c3e7fb3f602541add Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 8 Mar 2026 17:25:34 -0600 Subject: [PATCH 01/21] Add duplicate name suffix, nested participant populate, and auth error improvements - Allow duplicate participant names by appending random #XXX suffix on collision instead of blocking with 409 error (both guest and authenticated join flows) - Return populated participantIds (name, userId) in GET /users/:userId/events to enable frontend participant connection loading - Return LinkedIn-specific error message when a LinkedIn user tries local signup - Update all relevant docs (API reference, schema, realtime guide, roadmap) --- shatter-backend/docs/API_REFERENCE.md | 21 +- shatter-backend/docs/DATABASE_SCHEMA.md | 3 +- shatter-backend/docs/REALTIME_EVENTS_GUIDE.md | 4 +- shatter-backend/docs/name-bingo-roadmap.md | 1082 +++++++++++++++++ .../src/controllers/auth_controller.ts | 7 +- .../src/controllers/event_controller.ts | 75 +- .../src/controllers/user_controller.ts | 10 +- 7 files changed, 1166 insertions(+), 36 deletions(-) create mode 100644 shatter-backend/docs/name-bingo-roadmap.md diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index f1f50a8..097cdf7 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -1,6 +1,6 @@ # Shatter Backend — API Reference -**Last updated:** 2026-03-01 +**Last updated:** 2026-03-08 **Base URL:** `http://localhost:4000/api` --- @@ -169,7 +169,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 +421,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 | @@ -716,13 +723,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) --- @@ -767,13 +774,13 @@ Join an event as a guest (no account required). | 400 | `"Missing fields: guest name and eventId are required"` | | 400 | `"Event is full"` | | 404 | `"Event not found"` | -| 409 | `"This name is already taken in this event"` | **Special Behavior:** - Creates a guest User (`authProvider: 'guest'`, no email/password) +- 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) --- diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index 97b840b..a7ba2cd 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 @@ -157,6 +157,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. --- 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/src/controllers/auth_controller.ts b/shatter-backend/src/controllers/auth_controller.ts index 7d23c0c..dd8baff 100644 --- a/shatter-backend/src/controllers/auth_controller.ts +++ b/shatter-backend/src/controllers/auth_controller.ts @@ -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/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index c2c0c91..075a220 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -6,10 +6,44 @@ import "../models/participant_model"; import { generateJoinCode } from "../utils/event_utils"; import { generateToken } from "../utils/jwt_utils"; -import { Participant } from "../models/participant_model"; +import { Participant, IParticipant } from "../models/participant_model"; import { User } from "../models/user_model"; 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 @@ -159,22 +193,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 +230,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 +247,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, @@ -272,12 +300,17 @@ export async function joinEventAsGuest(req: Request, res: Response) { 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 +326,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 +345,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/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index 55e745b..a9a73fa 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import { User } from "../models/user_model"; +import "../models/participant_model"; import { hashPassword } from "../utils/password_hash"; 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) { From 78db3904f4f3b6d8b8d5b243d0c4011065b2625a Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 15 Mar 2026 17:48:21 -0600 Subject: [PATCH 02/21] Require at least one contact method for guest event join Guest users joining via POST /events/:eventId/join/guest must now provide either an email or at least one social link (LinkedIn, GitHub, or other). This ensures all participants are reachable after the event, which is core to the networking purpose of the app. --- shatter-backend/docs/API_REFERENCE.md | 18 ++++++++--- shatter-backend/docs/name-bingo-roadmap.md | 21 ++++++++++-- .../src/controllers/event_controller.ts | 32 ++++++++++++++++++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 097cdf7..813e1cc 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -747,9 +747,16 @@ 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 | + +\* At least one contact method is required: either `email` or at least one non-empty field in `socialLinks`. **Success Response (200):** @@ -772,11 +779,14 @@ 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 is required (email or a social link)"` | +| 400 | `"Invalid email format"` | | 400 | `"Event is full"` | | 404 | `"Event not found"` | +| 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 and/or social links) - 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` diff --git a/shatter-backend/docs/name-bingo-roadmap.md b/shatter-backend/docs/name-bingo-roadmap.md index ede1783..ce24c3c 100644 --- a/shatter-backend/docs/name-bingo-roadmap.md +++ b/shatter-backend/docs/name-bingo-roadmap.md @@ -159,8 +159,10 @@ These features are specified in `bingo_walkthrough.md` Section 3.1 as primary au **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) +- Takes `{ name, email?, socialLinks? }` in request body +- **Requires at least one contact method**: either `email` or at least one non-empty social link (`linkedin`, `github`, `other`) +- Validates email format if provided; rejects duplicate emails +- Creates a User record with `authProvider: 'guest'` and the provided contact info - Returns `{ success, participant, userId, token }` — guest gets a JWT immediately **Profile Completion**: `PUT /api/users/:userId` (protected, self-only) @@ -1080,3 +1082,18 @@ GET /api/bingo/:bingoId/state/:participantId - Get player's board state (prot POST /api/bingo/:bingoId/fill-cell - Fill a cell (protected) GET /api/events/:eventId/bingo/leaderboard - Get leaderboard (protected) ``` + +---------------- +personal notes from last meeting + +0.3 - Task B - Linkedin Account linking for guest users +0.3 - Task C - Connection acess control for gues users +after conversation with minh and keeryn, this would only be done on frontend + +-------- +leave event +delete/cancel event +get current event api + +--------- +speak with Jason about web implementation of event status on web diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 075a220..0e7f7d3 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -272,7 +272,11 @@ export async function joinEventAsUser(req: Request, res: Response) { */ export async function joinEventAsGuest(req: Request, res: Response) { try { - const { name } = req.body; + const { name, email, socialLinks } = req.body as { + name?: string; + email?: string; + socialLinks?: { linkedin?: string; github?: string; other?: string }; + }; const { eventId } = req.params; if (!name || !eventId) { @@ -282,6 +286,30 @@ export async function joinEventAsGuest(req: Request, res: Response) { }); } + // Require at least one contact method + const hasEmail = email && email.trim(); + const hasSocialLink = socialLinks && ( + socialLinks.linkedin?.trim() || + socialLinks.github?.trim() || + socialLinks.other?.trim() + ); + + if (!hasEmail && !hasSocialLink) { + return res.status(400).json({ + success: false, + msg: "At least one contact method is required (email or a social link)", + }); + } + + // 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" }); @@ -295,6 +323,8 @@ export async function joinEventAsGuest(req: Request, res: Response) { const user = await User.create({ name, authProvider: 'guest', + ...(hasEmail && { email: email.toLowerCase().trim() }), + ...(hasSocialLink && { socialLinks }), }); const userId = user._id as Types.ObjectId; From 60ffba279032327b4e5c6ead12dae65842c4fcfd Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 15 Mar 2026 17:51:12 -0600 Subject: [PATCH 03/21] Remove name-bingo-roadmap.md changes from previous commit The roadmap file is for local use only and should not be pushed. --- shatter-backend/docs/name-bingo-roadmap.md | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/shatter-backend/docs/name-bingo-roadmap.md b/shatter-backend/docs/name-bingo-roadmap.md index ce24c3c..ede1783 100644 --- a/shatter-backend/docs/name-bingo-roadmap.md +++ b/shatter-backend/docs/name-bingo-roadmap.md @@ -159,10 +159,8 @@ These features are specified in `bingo_walkthrough.md` Section 3.1 as primary au **Approach**: Instead of a separate signup endpoint, guest users are created automatically when joining an event. **Endpoint**: `POST /api/events/:eventId/join/guest` -- Takes `{ name, email?, socialLinks? }` in request body -- **Requires at least one contact method**: either `email` or at least one non-empty social link (`linkedin`, `github`, `other`) -- Validates email format if provided; rejects duplicate emails -- Creates a User record with `authProvider: 'guest'` and the provided contact info +- 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) @@ -1082,18 +1080,3 @@ GET /api/bingo/:bingoId/state/:participantId - Get player's board state (prot POST /api/bingo/:bingoId/fill-cell - Fill a cell (protected) GET /api/events/:eventId/bingo/leaderboard - Get leaderboard (protected) ``` - ----------------- -personal notes from last meeting - -0.3 - Task B - Linkedin Account linking for guest users -0.3 - Task C - Connection acess control for gues users -after conversation with minh and keeryn, this would only be done on frontend - --------- -leave event -delete/cancel event -get current event api - ---------- -speak with Jason about web implementation of event status on web From 306ebf88985cf03f466b9f70773514b7745631e9 Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 15 Mar 2026 19:25:10 -0600 Subject: [PATCH 04/21] Add organization and title fields to User model with guest fallback validation Guests who skip sharing contact info can now provide an organization as an alternative. Both organization and title are optional fields available on all user profiles via the update endpoint. --- shatter-backend/docs/API_REFERENCE.md | 13 +++++++++---- shatter-backend/docs/DATABASE_SCHEMA.md | 2 ++ shatter-backend/src/controllers/event_controller.ts | 13 +++++++++---- shatter-backend/src/controllers/user_controller.ts | 6 +++++- shatter-backend/src/models/user_model.ts | 10 ++++++++++ 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 813e1cc..41d4dcd 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -463,6 +463,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):** @@ -755,8 +757,10 @@ Join an event as a guest (no account required). | `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 contact method is required: either `email` or at least one non-empty field in `socialLinks`. +\* At least one of the following is required: `email`, a non-empty field in `socialLinks`, or `organization`. **Success Response (200):** @@ -779,14 +783,14 @@ 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 is required (email or a social link)"` | +| 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 | `"A user with this email already exists"` | **Special Behavior:** -- Creates a guest User (`authProvider: 'guest'`) with the provided contact info (email and/or social links) +- 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` @@ -1259,7 +1263,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 a7ba2cd..21d9dc6 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -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 }` | diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 0e7f7d3..1d9cf9c 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -272,10 +272,12 @@ export async function joinEventAsUser(req: Request, res: Response) { */ export async function joinEventAsGuest(req: Request, res: Response) { try { - const { name, email, socialLinks } = req.body as { + 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 { eventId } = req.params; @@ -286,18 +288,19 @@ export async function joinEventAsGuest(req: Request, res: Response) { }); } - // Require at least one contact method + // 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) { + if (!hasEmail && !hasSocialLink && !hasOrganization) { return res.status(400).json({ success: false, - msg: "At least one contact method is required (email or a social link)", + msg: "At least one contact method (email or social link) or organization is required", }); } @@ -325,6 +328,8 @@ export async function joinEventAsGuest(req: Request, res: Response) { 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; diff --git a/shatter-backend/src/controllers/user_controller.ts b/shatter-backend/src/controllers/user_controller.ts index a9a73fa..1c2115a 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -114,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 = {}; @@ -161,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/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, From b5b5f238394f6e6c4f0bc3ac09d01e7af1784a80 Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 19 Mar 2026 16:39:40 -0600 Subject: [PATCH 05/21] Fix MongoDB connection timeout on Vercel serverless Move DB connection middleware before route handlers in app.ts to ensure the connection is verified before any route handler fires. Extract shared connection logic into utils/db.ts with ping-based stale connection detection to handle Vercel freeze/thaw cycles that leave dead TCP sockets despite readyState showing connected. Tune connection options for serverless (bufferCommands: false, smaller pool, heartbeat). --- shatter-backend/api/index.ts | 70 +++------------------------ shatter-backend/src/app.ts | 4 ++ shatter-backend/src/server.ts | 4 +- shatter-backend/src/utils/db.ts | 84 +++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 66 deletions(-) create mode 100644 shatter-backend/src/utils/db.ts diff --git a/shatter-backend/api/index.ts b/shatter-backend/api/index.ts index 655999c..745313b 100644 --- a/shatter-backend/api/index.ts +++ b/shatter-backend/api/index.ts @@ -1,71 +1,13 @@ import 'dotenv/config'; -import mongoose from 'mongoose'; +import { connectDB } from '../src/utils/db'; import app from '../src/app'; const MONGODB_URI = process.env.MONGO_URI; - -let connectionPromise: Promise | null = null; - -async function connectDB() { - if (mongoose.connection.readyState === 1) { - return; - } - - if (mongoose.connection.readyState !== 0) { - await mongoose.disconnect(); - connectionPromise = null; - } - - if (connectionPromise) { - return connectionPromise; - } - - if (!MONGODB_URI) { - throw new Error('MONGO_URI is not set in environment variables'); - } - - connectionPromise = (async () => { - try { - await mongoose.connect(MONGODB_URI, { - maxPoolSize: 10, - minPoolSize: 2, - maxIdleTimeMS: 30000, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000, - }); - - mongoose.connection.on('error', (err) => { - console.error('MongoDB connection error:', err); - connectionPromise = null; - }); - - mongoose.connection.on('disconnected', () => { - console.log('MongoDB disconnected'); - connectionPromise = null; - }); - - } catch (error) { - console.error('Failed to connect to MongoDB:', error); - connectionPromise = null; - throw error; - } - })(); - - return connectionPromise; +if (!MONGODB_URI) { + throw new Error('MONGO_URI is not set in environment variables'); } -connectDB().catch(console.error); - -app.use(async (req, res, next) => { - try { - await connectDB(); - next(); - } catch (error: any) { - res.status(500).json({ - error: 'Database connection failed', - message: error.message - }); - } -}); +// Eagerly start connection at module load (Vercel cold start) +connectDB(MONGODB_URI).catch(console.error); -export default app; \ No newline at end of file +export default app; diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 7f5f7ea..3c0df13 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,6 +1,7 @@ import express from "express"; import cors from "cors"; +import { ensureConnection } from "./utils/db"; import userRoutes from './routes/user_route'; import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; @@ -40,6 +41,9 @@ app.get("/", (_req, res) => { res.send("Hello"); }); +// Ensure DB connection is alive before handling any API request +app.use("/api", ensureConnection); + app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); app.use('/api/events', eventRoutes); diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index e209977..2c568d5 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import mongoose from "mongoose"; +import { connectDB } from "./utils/db"; import app from "./app"; const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; @@ -25,7 +25,7 @@ async function start() { if (!MONGODB_URI) { throw new Error("MONGO_URI is not set"); } - await mongoose.connect(MONGODB_URI); + await connectDB(MONGODB_URI); console.log("Successfully connected to MongoDB"); app.listen(PORT, () => { diff --git a/shatter-backend/src/utils/db.ts b/shatter-backend/src/utils/db.ts new file mode 100644 index 0000000..2e5f65e --- /dev/null +++ b/shatter-backend/src/utils/db.ts @@ -0,0 +1,84 @@ +import mongoose from "mongoose"; +import { Request, Response, NextFunction } from "express"; + +let connectionPromise: Promise | null = null; +let listenersRegistered = false; + +function registerListeners(): void { + if (listenersRegistered) return; + listenersRegistered = true; + + mongoose.connection.on("connected", () => console.log("MongoDB: connected")); + mongoose.connection.on("disconnected", () => { + console.log("MongoDB: disconnected"); + connectionPromise = null; + }); + mongoose.connection.on("reconnected", () => console.log("MongoDB: reconnected")); + mongoose.connection.on("error", (err) => { + console.error("MongoDB: error:", err); + connectionPromise = null; + }); +} + +export async function connectDB(uri: string): Promise { + registerListeners(); + + // If connected, verify the connection is actually alive (not stale from serverless freeze) + if (mongoose.connection.readyState === 1) { + try { + await mongoose.connection.db!.admin().ping(); + return; // genuinely alive + } catch { + console.log("MongoDB connection stale, reconnecting..."); + await mongoose.disconnect(); + connectionPromise = null; + } + } + + // If in a transitional state (connecting/disconnecting), reset + if (mongoose.connection.readyState !== 0) { + await mongoose.disconnect(); + connectionPromise = null; + } + + // Reuse in-flight connection attempt + if (connectionPromise) { + return connectionPromise; + } + + connectionPromise = (async () => { + try { + await mongoose.connect(uri, { + bufferCommands: false, + maxPoolSize: 5, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 30000, + heartbeatFrequencyMS: 10000, + }); + console.log("MongoDB connected"); + } catch (error) { + console.error("MongoDB connection failed:", error); + connectionPromise = null; + throw error; + } + })(); + + return connectionPromise; +} + +export function ensureConnection(req: Request, res: Response, next: NextFunction): void { + const uri = process.env.MONGO_URI; + if (!uri) { + res.status(500).json({ error: "MONGO_URI is not configured" }); + return; + } + + connectDB(uri) + .then(() => next()) + .catch((error: any) => { + res.status(500).json({ + error: "Database connection failed", + message: error.message, + }); + }); +} From a8a54f4ab786a95ff730ca98d1e00688480d9ac2 Mon Sep 17 00:00:00 2001 From: rxmox Date: Thu, 19 Mar 2026 17:23:49 -0600 Subject: [PATCH 06/21] Fix TypeScript error for req.params.eventId type mismatch Cast req.params.eventId to string in event controller to fix TS2345 where string | string[] was passed to functions expecting string. --- shatter-backend/src/controllers/event_controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 1d9cf9c..223a6be 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -170,7 +170,7 @@ 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 eventId = req.params.eventId as string; if (!userId || !name || !eventId) return res.status(400).json({ @@ -279,7 +279,7 @@ export async function joinEventAsGuest(req: Request, res: Response) { organization?: string; title?: string; }; - const { eventId } = req.params; + const eventId = req.params.eventId as string; if (!name || !eventId) { return res.status(400).json({ @@ -404,7 +404,7 @@ export async function joinEventAsGuest(req: Request, res: Response) { */ export async function getEventById(req: Request, res: Response) { try { - const { eventId } = req.params; + const eventId = req.params.eventId as string; if (!eventId) { return res @@ -455,7 +455,7 @@ export async function getEventById(req: Request, res: Response) { */ export async function updateEventStatus(req: Request, res: Response) { try { - const { eventId } = req.params; + const eventId = req.params.eventId as string; const { status } = req.body; const validStatuses = ['In Progress', 'Completed']; From 41d03c03c6bcd2a2afbf4e9c027836f78b437dd3 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 19 Mar 2026 16:35:55 -0600 Subject: [PATCH 07/21] Added new format for bing_grid response to include a short version of each bingo question --- .../src/ai/prompts/bingo_short_questions.txt | 2 + .../src/controllers/bingo_controller.ts | 71 ++++++++++++++++--- 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 shatter-backend/src/ai/prompts/bingo_short_questions.txt 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..0915529 --- /dev/null +++ b/shatter-backend/src/ai/prompts/bingo_short_questions.txt @@ -0,0 +1,2 @@ +You will generate the bingo grid with shortend 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 quesiton is about. \ No newline at end of file diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 70c16f2..4225697 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -198,8 +198,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 +224,6 @@ function buildSchema(rows: number, cols: number): z.ZodObject = {}; @@ -244,7 +241,6 @@ 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); @@ -267,9 +263,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,8 +276,52 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string }, }); - console.log("RAW RESPONSE:"); - console.log(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) + } +} + +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, + }, + }); const parsed = JSON.parse(response.text!); const validated: Record = schema.parse(parsed); @@ -315,6 +352,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] + })) + ); +} + /** * POST /api/bingo/generate * @@ -352,8 +398,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, From 1e3c01a755d8a9c658c853dcd7eb7880f8f110c8 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 19 Mar 2026 20:36:44 -0600 Subject: [PATCH 08/21] fixed some type bugs --- shatter-backend/src/controllers/event_controller.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 1d9cf9c..efcb0bd 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -170,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({ @@ -279,7 +280,8 @@ export async function joinEventAsGuest(req: Request, res: Response) { organization?: string; title?: string; }; - const { eventId } = req.params; + const rawEventId = req.params.eventId; + const eventId = Array.isArray(rawEventId) ? rawEventId[0] : rawEventId; if (!name || !eventId) { return res.status(400).json({ From 09d894d6d8d72eabfd3f1497981b75147e36d862 Mon Sep 17 00:00:00 2001 From: Inko-z Date: Thu, 19 Mar 2026 20:50:54 -0600 Subject: [PATCH 09/21] added bingo route documentation --- shatter-backend/docs/API_REFERENCE.md | 138 +++++++++++++++++++------- 1 file changed, 102 insertions(+), 36 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 41d4dcd..5138dd3 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -7,42 +7,55 @@ ## 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) --- @@ -953,6 +966,59 @@ Update a bingo game. --- +### POST `/api/bingo/generate` + +Generate an AI-powered bingo grid based on a given context. + +- **Auth:** Not specified (assumed Public unless otherwise enforced) + +**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/` From e89c83aba895e1c832b55260f6fe411997e6d128 Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 22 Mar 2026 17:13:15 -0600 Subject: [PATCH 10/21] Fix bingo model schema, validation, and error handling for BingoTile format - Update Bingo model to store { question, shortQuestion } objects instead of plain strings, matching the mobile BingoTile interface - Update grid validation in createBingo and updateBingo to accept the new format - Add null guard for Gemini response.text in both AI generation functions - Add bounds checking in combine2DArrays to fallback to full question when short version is missing - Fix typos in bingo_short_questions.txt prompt - Remove unused Console import --- .../src/ai/prompts/bingo_short_questions.txt | 4 +- .../src/controllers/bingo_controller.ts | 51 +++++++++++++------ shatter-backend/src/models/bingo_model.ts | 14 ++++- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/shatter-backend/src/ai/prompts/bingo_short_questions.txt b/shatter-backend/src/ai/prompts/bingo_short_questions.txt index 0915529..8b12320 100644 --- a/shatter-backend/src/ai/prompts/bingo_short_questions.txt +++ b/shatter-backend/src/ai/prompts/bingo_short_questions.txt @@ -1,2 +1,2 @@ -You will generate the bingo grid with shortend 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 quesiton is about. \ No newline at end of file +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/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 4225697..41f5b45 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -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 }", }); } } @@ -149,18 +154,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 }", }); } @@ -276,13 +287,17 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string }, }); - const parsed = JSON.parse(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) + console.error("Generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) } } @@ -323,13 +338,17 @@ async function generateBingoGrid_shortVersions(n_rows: number, n_cols: number, o }, }); - const parsed = JSON.parse(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) + console.error("Short version generation failed:", error); + return makeEmptyGrid(n_rows, n_cols) } } @@ -356,7 +375,7 @@ function combine2DArrays(arr1: string[][], arr2: string[][]): { question: string return arr1.map((row, i) => row.map((val, j) => ({ question: val, - shortQuestion: arr2[i][j] + shortQuestion: arr2[i]?.[j] || val, })) ); } 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, From b3d627ba3b675fbaf7ef60ba1847f5b0614ba72d Mon Sep 17 00:00:00 2001 From: rxmox Date: Sun, 22 Mar 2026 17:18:38 -0600 Subject: [PATCH 11/21] Update bingo docs to reflect BingoTile grid format - Update createBingo, getBingo, updateBingo docs with { question, shortQuestion } grid format - Update error messages to match new validation - Fix auth status on generate endpoint (is Protected, not Public) - Update DATABASE_SCHEMA.md grid type from [[String]] to BingoTile objects --- shatter-backend/docs/API_REFERENCE.md | 38 ++++++++++++++++--------- shatter-backend/docs/DATABASE_SCHEMA.md | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/shatter-backend/docs/API_REFERENCE.md b/shatter-backend/docs/API_REFERENCE.md index 5138dd3..a51ac2f 100644 --- a/shatter-backend/docs/API_REFERENCE.md +++ b/shatter-backend/docs/API_REFERENCE.md @@ -861,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):** @@ -876,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" } + ] ] } } @@ -890,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"` | --- @@ -916,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" } + ] + ] } } ``` @@ -939,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):** @@ -958,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"` | @@ -970,7 +982,7 @@ Update a bingo game. Generate an AI-powered bingo grid based on a given context. -- **Auth:** Not specified (assumed Public unless otherwise enforced) +- **Auth:** Protected **Request Body:** diff --git a/shatter-backend/docs/DATABASE_SCHEMA.md b/shatter-backend/docs/DATABASE_SCHEMA.md index 21d9dc6..e68af11 100644 --- a/shatter-backend/docs/DATABASE_SCHEMA.md +++ b/shatter-backend/docs/DATABASE_SCHEMA.md @@ -177,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 From c6ddefa879f139049ab9cd4e02909341a0cdbd49 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:30:27 -0600 Subject: [PATCH 12/21] Vercel bundle correctly source to deploy --- shatter-backend/vercel.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shatter-backend/vercel.json b/shatter-backend/vercel.json index 9b2aa1f..35a494d 100644 --- a/shatter-backend/vercel.json +++ b/shatter-backend/vercel.json @@ -11,5 +11,10 @@ "src": "/(.*)", "dest": "dist/api/index.js" } - ] -} \ No newline at end of file + ], + "functions": { + "api/index.ts": { + "includeFiles": "src/**" + } + } +} From 4bfbd957400e15cfb1ea34b48af5b0bb1a508f33 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:36:02 -0600 Subject: [PATCH 13/21] use modern approach for vercel deployment --- shatter-backend/vercel.json | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/shatter-backend/vercel.json b/shatter-backend/vercel.json index 35a494d..12333e9 100644 --- a/shatter-backend/vercel.json +++ b/shatter-backend/vercel.json @@ -1,20 +1,8 @@ { - "version": 2, - "builds": [ - { - "src": "dist/api/index.js", - "use": "@vercel/node" - } - ], - "routes": [ - { - "src": "/(.*)", - "dest": "dist/api/index.js" - } - ], "functions": { "api/index.ts": { + "runtime": "nodejs20.x", "includeFiles": "src/**" } } -} +} \ No newline at end of file From 3699772ac2f0cbe392b159e482ee9ef878d8bbdb Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:42:14 -0600 Subject: [PATCH 14/21] change CD backend logic --- .github/workflows/cd-backend.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/cd-backend.yml b/.github/workflows/cd-backend.yml index b700594..7973581 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 + vercel deploy --prod --force --token=$VERCEL_TOKEN From 202a37b5d678b0b03db70cda1010c42a45721475 Mon Sep 17 00:00:00 2001 From: Minh Le <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:01:52 -0600 Subject: [PATCH 15/21] Revert "Fix MongoDB connection timeout on Vercel serverless" --- shatter-backend/api/index.ts | 70 ++++++++++++++-- shatter-backend/src/app.ts | 4 - .../src/controllers/event_controller.ts | 8 +- shatter-backend/src/server.ts | 4 +- shatter-backend/src/utils/db.ts | 84 ------------------- 5 files changed, 70 insertions(+), 100 deletions(-) delete mode 100644 shatter-backend/src/utils/db.ts diff --git a/shatter-backend/api/index.ts b/shatter-backend/api/index.ts index 745313b..655999c 100644 --- a/shatter-backend/api/index.ts +++ b/shatter-backend/api/index.ts @@ -1,13 +1,71 @@ import 'dotenv/config'; -import { connectDB } from '../src/utils/db'; +import mongoose from 'mongoose'; import app from '../src/app'; const MONGODB_URI = process.env.MONGO_URI; -if (!MONGODB_URI) { - throw new Error('MONGO_URI is not set in environment variables'); + +let connectionPromise: Promise | null = null; + +async function connectDB() { + if (mongoose.connection.readyState === 1) { + return; + } + + if (mongoose.connection.readyState !== 0) { + await mongoose.disconnect(); + connectionPromise = null; + } + + if (connectionPromise) { + return connectionPromise; + } + + if (!MONGODB_URI) { + throw new Error('MONGO_URI is not set in environment variables'); + } + + connectionPromise = (async () => { + try { + await mongoose.connect(MONGODB_URI, { + maxPoolSize: 10, + minPoolSize: 2, + maxIdleTimeMS: 30000, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + }); + + mongoose.connection.on('error', (err) => { + console.error('MongoDB connection error:', err); + connectionPromise = null; + }); + + mongoose.connection.on('disconnected', () => { + console.log('MongoDB disconnected'); + connectionPromise = null; + }); + + } catch (error) { + console.error('Failed to connect to MongoDB:', error); + connectionPromise = null; + throw error; + } + })(); + + return connectionPromise; } -// Eagerly start connection at module load (Vercel cold start) -connectDB(MONGODB_URI).catch(console.error); +connectDB().catch(console.error); + +app.use(async (req, res, next) => { + try { + await connectDB(); + next(); + } catch (error: any) { + res.status(500).json({ + error: 'Database connection failed', + message: error.message + }); + } +}); -export default app; +export default app; \ No newline at end of file diff --git a/shatter-backend/src/app.ts b/shatter-backend/src/app.ts index 3c0df13..7f5f7ea 100644 --- a/shatter-backend/src/app.ts +++ b/shatter-backend/src/app.ts @@ -1,7 +1,6 @@ import express from "express"; import cors from "cors"; -import { ensureConnection } from "./utils/db"; import userRoutes from './routes/user_route'; import authRoutes from './routes/auth_routes'; import eventRoutes from './routes/event_routes'; @@ -41,9 +40,6 @@ app.get("/", (_req, res) => { res.send("Hello"); }); -// Ensure DB connection is alive before handling any API request -app.use("/api", ensureConnection); - app.use('/api/users', userRoutes); app.use('/api/auth', authRoutes); app.use('/api/events', eventRoutes); diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index 223a6be..1d9cf9c 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -170,7 +170,7 @@ 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.eventId as string; + const { eventId } = req.params; if (!userId || !name || !eventId) return res.status(400).json({ @@ -279,7 +279,7 @@ export async function joinEventAsGuest(req: Request, res: Response) { organization?: string; title?: string; }; - const eventId = req.params.eventId as string; + const { eventId } = req.params; if (!name || !eventId) { return res.status(400).json({ @@ -404,7 +404,7 @@ export async function joinEventAsGuest(req: Request, res: Response) { */ export async function getEventById(req: Request, res: Response) { try { - const eventId = req.params.eventId as string; + const { eventId } = req.params; if (!eventId) { return res @@ -455,7 +455,7 @@ export async function getEventById(req: Request, res: Response) { */ export async function updateEventStatus(req: Request, res: Response) { try { - const eventId = req.params.eventId as string; + const { eventId } = req.params; const { status } = req.body; const validStatuses = ['In Progress', 'Completed']; diff --git a/shatter-backend/src/server.ts b/shatter-backend/src/server.ts index 2c568d5..e209977 100644 --- a/shatter-backend/src/server.ts +++ b/shatter-backend/src/server.ts @@ -1,5 +1,5 @@ import "dotenv/config"; -import { connectDB } from "./utils/db"; +import mongoose from "mongoose"; import app from "./app"; const PORT = process.env.PORT ? Number(process.env.PORT) : 4000; @@ -25,7 +25,7 @@ async function start() { if (!MONGODB_URI) { throw new Error("MONGO_URI is not set"); } - await connectDB(MONGODB_URI); + await mongoose.connect(MONGODB_URI); console.log("Successfully connected to MongoDB"); app.listen(PORT, () => { diff --git a/shatter-backend/src/utils/db.ts b/shatter-backend/src/utils/db.ts deleted file mode 100644 index 2e5f65e..0000000 --- a/shatter-backend/src/utils/db.ts +++ /dev/null @@ -1,84 +0,0 @@ -import mongoose from "mongoose"; -import { Request, Response, NextFunction } from "express"; - -let connectionPromise: Promise | null = null; -let listenersRegistered = false; - -function registerListeners(): void { - if (listenersRegistered) return; - listenersRegistered = true; - - mongoose.connection.on("connected", () => console.log("MongoDB: connected")); - mongoose.connection.on("disconnected", () => { - console.log("MongoDB: disconnected"); - connectionPromise = null; - }); - mongoose.connection.on("reconnected", () => console.log("MongoDB: reconnected")); - mongoose.connection.on("error", (err) => { - console.error("MongoDB: error:", err); - connectionPromise = null; - }); -} - -export async function connectDB(uri: string): Promise { - registerListeners(); - - // If connected, verify the connection is actually alive (not stale from serverless freeze) - if (mongoose.connection.readyState === 1) { - try { - await mongoose.connection.db!.admin().ping(); - return; // genuinely alive - } catch { - console.log("MongoDB connection stale, reconnecting..."); - await mongoose.disconnect(); - connectionPromise = null; - } - } - - // If in a transitional state (connecting/disconnecting), reset - if (mongoose.connection.readyState !== 0) { - await mongoose.disconnect(); - connectionPromise = null; - } - - // Reuse in-flight connection attempt - if (connectionPromise) { - return connectionPromise; - } - - connectionPromise = (async () => { - try { - await mongoose.connect(uri, { - bufferCommands: false, - maxPoolSize: 5, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 30000, - heartbeatFrequencyMS: 10000, - }); - console.log("MongoDB connected"); - } catch (error) { - console.error("MongoDB connection failed:", error); - connectionPromise = null; - throw error; - } - })(); - - return connectionPromise; -} - -export function ensureConnection(req: Request, res: Response, next: NextFunction): void { - const uri = process.env.MONGO_URI; - if (!uri) { - res.status(500).json({ error: "MONGO_URI is not configured" }); - return; - } - - connectDB(uri) - .then(() => next()) - .catch((error: any) => { - res.status(500).json({ - error: "Database connection failed", - message: error.message, - }); - }); -} From c817585ef05a3b2aaa4ee1876bb8fcad792298b9 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:05:03 -0600 Subject: [PATCH 16/21] reverse vercel.json --- shatter-backend/vercel.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shatter-backend/vercel.json b/shatter-backend/vercel.json index 12333e9..9b2aa1f 100644 --- a/shatter-backend/vercel.json +++ b/shatter-backend/vercel.json @@ -1,8 +1,15 @@ { - "functions": { - "api/index.ts": { - "runtime": "nodejs20.x", - "includeFiles": "src/**" + "version": 2, + "builds": [ + { + "src": "dist/api/index.js", + "use": "@vercel/node" } - } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "dist/api/index.js" + } + ] } \ No newline at end of file From 74ea597f3f4af8377507b5217f0dfecb0b8941bb Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:12:43 -0600 Subject: [PATCH 17/21] fix get bingo game by eventId --- shatter-backend/src/controllers/bingo_controller.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 41f5b45..9cb6cab 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -103,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({ From 4d799a2f9f8fa9a483e453c105b9b2941a77050c Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:52:14 -0600 Subject: [PATCH 18/21] fix the typescript config --- shatter-backend/package.json | 4 ++-- shatter-backend/tsconfig.json | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/shatter-backend/package.json b/shatter-backend/package.json index 4bc301b..d289a90 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -4,7 +4,7 @@ "main": "index.js", "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" }, @@ -36,7 +36,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/tsconfig.json b/shatter-backend/tsconfig.json index 532b6dc..c997406 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -1,17 +1,15 @@ { "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", + "target": "ES2021", + "module": "node16", + "moduleResolution": "node16", "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": ["ES2021"], + "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"], "exclude": ["node_modules", "dist"] From fd10952c2a09d5fe143ae8fb106a6a0b34c3b886 Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:56:26 -0600 Subject: [PATCH 19/21] fix: sync lock file --- shatter-backend/package-lock.json | 1177 ++++++++++++++++------------- 1 file changed, 647 insertions(+), 530 deletions(-) diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 16b7d53..580ef9b 100644 --- a/shatter-backend/package-lock.json +++ b/shatter-backend/package-lock.json @@ -32,451 +32,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" - ], + "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": [ - "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" - ], - "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": { @@ -728,6 +299,34 @@ "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/@mongodb-js/saslprep": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", @@ -817,6 +416,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 +615,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 +983,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", @@ -1395,6 +1049,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 +1160,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 +1208,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 +1236,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 +1308,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 +1440,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 +1512,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 +1548,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", @@ -1937,56 +1723,14 @@ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "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" + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "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": ">= 0.4" } }, "node_modules/escape-html": { @@ -2304,6 +2048,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 +2223,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 +2320,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", @@ -2813,6 +2564,18 @@ "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", @@ -2838,6 +2601,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 +2662,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 +2682,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" }, @@ -3116,6 +2919,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 +3000,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 +3019,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 +3068,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 +3082,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 +3099,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 +3114,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 +3175,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 +3197,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 +3302,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 +3455,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 +3474,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", @@ -3812,24 +3680,61 @@ "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/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 +4082,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 +4217,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 +4253,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 +4283,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 +4317,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 +4340,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 +4580,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 +4767,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", From b61e366814ff10815396a8b47f311d86d9d6baad Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:20:59 -0600 Subject: [PATCH 20/21] update module typescript --- shatter-backend/package-lock.json | 221 +++++++++++++++++- shatter-backend/package.json | 2 + .../src/controllers/bingo_controller.ts | 2 + shatter-backend/tsconfig.json | 6 +- 4 files changed, 227 insertions(+), 4 deletions(-) diff --git a/shatter-backend/package-lock.json b/shatter-backend/package-lock.json index 580ef9b..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", @@ -230,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", @@ -327,6 +340,68 @@ "@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", @@ -1022,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", @@ -1924,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", @@ -1967,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", @@ -1977,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": { @@ -1994,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", @@ -2478,6 +2646,15 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2582,6 +2759,15 @@ "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", @@ -2723,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", @@ -2759,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", @@ -3520,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", @@ -3706,6 +3916,15 @@ "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", diff --git a/shatter-backend/package.json b/shatter-backend/package.json index d289a90..b4240b0 100644 --- a/shatter-backend/package.json +++ b/shatter-backend/package.json @@ -2,6 +2,7 @@ "name": "shatter-backend", "version": "1.0.0", "main": "index.js", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "ts-node-dev --respawn --transpile-only src/server.ts", @@ -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", diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 9cb6cab..907351d 100644 --- a/shatter-backend/src/controllers/bingo_controller.ts +++ b/shatter-backend/src/controllers/bingo_controller.ts @@ -253,6 +253,8 @@ async function generateBingoGrid(n_rows: number, n_cols: number, context: string 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: diff --git a/shatter-backend/tsconfig.json b/shatter-backend/tsconfig.json index c997406..a8bb36d 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "target": "ES2021", - "module": "node16", - "moduleResolution": "node16", + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "sourceMap": true, From 48eacce690edaf03d0bbaf7b24caea0d882964da Mon Sep 17 00:00:00 2001 From: Le Nguyen Quang Minh <101281380+lnqminh3003@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:42:30 -0600 Subject: [PATCH 21/21] config ts --- shatter-backend/api/index.ts | 4 ++-- shatter-backend/src/app.ts | 10 +++++----- shatter-backend/src/controllers/auth_controller.ts | 10 +++++----- .../src/controllers/bingo_controller.ts | 8 ++++---- .../src/controllers/event_controller.ts | 14 +++++++------- .../participant_connections_controller.ts | 10 +++++----- shatter-backend/src/controllers/user_controller.ts | 6 +++--- shatter-backend/src/middleware/auth_middleware.ts | 2 +- shatter-backend/src/models/event_model.ts | 4 ++-- shatter-backend/src/routes/auth_routes.ts | 2 +- shatter-backend/src/routes/bingo_routes.ts | 4 ++-- shatter-backend/src/routes/event_routes.ts | 4 ++-- .../src/routes/participant_connections_routes.ts | 6 +++--- shatter-backend/src/routes/user_route.ts | 6 +++--- shatter-backend/src/server.ts | 2 +- shatter-backend/src/utils/test_jwt.ts | 2 +- shatter-backend/src/utils/test_password.ts | 2 +- shatter-backend/tsconfig.json | 3 ++- 18 files changed, 50 insertions(+), 49 deletions(-) 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/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 dd8baff..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 || ''; diff --git a/shatter-backend/src/controllers/bingo_controller.ts b/shatter-backend/src/controllers/bingo_controller.ts index 907351d..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"; diff --git a/shatter-backend/src/controllers/event_controller.ts b/shatter-backend/src/controllers/event_controller.ts index efcb0bd..4a7401a 100644 --- a/shatter-backend/src/controllers/event_controller.ts +++ b/shatter-backend/src/controllers/event_controller.ts @@ -1,13 +1,13 @@ 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, IParticipant } 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"; /** 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 1c2115a..2b0796c 100644 --- a/shatter-backend/src/controllers/user_controller.ts +++ b/shatter-backend/src/controllers/user_controller.ts @@ -1,7 +1,7 @@ import { Request, Response } from "express"; -import { User } from "../models/user_model"; -import "../models/participant_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,}$/; 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/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/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 a8bb36d..0dee230 100644 --- a/shatter-backend/tsconfig.json +++ b/shatter-backend/tsconfig.json @@ -8,7 +8,8 @@ "sourceMap": true, "outDir": "./dist", "rootDir": "./", - "lib": ["ES2021"], + "lib": ["ES2020", "DOM"], + "skipLibCheck": true, "typeRoots": ["./src/types", "./node_modules/@types"] }, "include": ["src/**/*", "api/**/*"],