diff --git a/.env.example b/.env.example index 3a3ded2..bd141ac 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,12 @@ FRONTEND_PORT=5173 # Frontend proxy target VITE_BACKEND_TARGET=http://localhost:3000 + +# Database +DATABASE_URL=URL_GOES_HERE +SESSION_SECRET=REPLACE_ME_WITH_RANDOM +SESSION_COOKIE_NAME=gs_session +SESSION_TTL_DAYS=30 + +# Testing +DATABASE_URL_TEST=URL_GOES_HERE_FOR_TESTING \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94da84c..b111dbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,21 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: gameswipe + POSTGRES_PASSWORD: gameswipe + POSTGRES_DB: gameswipe_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U gameswipe -d gameswipe_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout uses: actions/checkout@v4 @@ -32,8 +47,20 @@ jobs: - name: Lint run: npm run lint + - name: Install psql + run: sudo apt-get update && sudo apt-get install -y postgresql-client + + - name: Migrate DB (apply SQL) + run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f apps/backend/migrations/001_init.sql + env: + DATABASE_URL: postgresql://gameswipe:gameswipe@localhost:5432/gameswipe_test + - name: Test run: npm run test + env: + DATABASE_URL: postgresql://gameswipe:gameswipe@localhost:5432/gameswipe_test + DATABASE_URL_TEST: postgresql://gameswipe:gameswipe@localhost:5432/gameswipe_test + SESSION_SECRET: "0123456789abcdef0123456789abcdef" - name: Build run: npm run build diff --git a/.gitignore b/.gitignore index 79ec1f7..537bc52 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage .env .env.* !.env.example +!.env.test .DS_Store # Vite / tooling @@ -23,3 +24,6 @@ coverage **/vite.config.js **/vite.config.d.ts **/vite.config.d.ts.map + +# any temp files or folders +**/.tmp \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..fba9b6e --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,5 @@ +{ + "no-duplicate-heading": { + "siblings_only": true + } +} diff --git a/README.md b/README.md index cfbdd42..cb7df16 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ A Swipe Web App for deciding what Steam game to play with a group of friends. Monorepo: -- `apps/frontend` — React + Vite + TypeScript -- `apps/backend` — Express + TypeScript -- `packages/shared` — shared types/schemas +- `apps/frontend` - React + Vite + TypeScript +- `apps/backend` - Express + TypeScript +- `packages/shared` - shared types/schemas ## Local dev @@ -14,3 +14,21 @@ Monorepo: npm install npm run dev ``` + +### Useful Commands + +```BASH +npm run lint +npm run build +npm run test +npm run format:check +npm run format +``` + +### Frontend + +Refer to [Frontend README](apps/frontend/README.md) + +### Backend + +Refer to [BACKEND README](apps/backend/README.md) diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..d00d707 --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,260 @@ +# GameSwipe Backend (apps/backend) + +Express + TypeScript backend for GameSwipe (room-based multiplayer game picker). + +This README documents the currently implemented endpoints: + +- Health +- Create room +- Join room +- Get room state +- Delete room + +## Run locally + +### Prerequisites + +- Node.js +- A Postgres database (e.g. Supabase) + +### Environment variables + +These are read from the repo root `.env` (recommended): + +```env +PORT=4000 + +DATABASE_URL=postgresql://... +SESSION_SECRET=...at least 16 chars... + +SESSION_COOKIE_NAME=gs_session +SESSION_TTL_DAYS=30 +Start dev server +``` + +## Run from GameSwipe repo root + +```BASH +npm --workspace apps/backend run dev +``` + +Server listens on: + +- + +## Common API behavior + +### Content-Type + +All JSON endpoints use: + +- Content-Type: application/json + +### Session auth (cookie) + +Authenticated endpoints require a session cookie: + +- Cookie name: SESSION_COOKIE_NAME (default gs_session) + +- Cookie is set on successful room create/join. + +### Error response format + +On errors, responses follow: + +```JSON +{ + "error": { + "code": "SOME_CODE", + "message": "Human readable message", + "details": null + } +} +``` + +Common status codes: + +- 400 validation errors (e.g. invalid JSON shape) + +- 401 unauthenticated (missing/invalid/expired session) + +- 403 authenticated but forbidden (not a room member, not creator) + +- 404 not found (room does not exist / code not found) + +- 410 gone (room deleted or expired) + +- 500 unexpected server error + +## Endpoints + +### GET `/health` + +Simple health check. + +**Response `200`** + +```JSON +{ + "ok": true, + "service": "gameswipe-backend", + "time": "2026-02-20T12:59:55.000Z" +} +``` + +### POST /rooms + +Create a new room and a creator member. Sets a session cookie. + +```JSON +Request body +{ + "expiresInHours": 24 +} +``` + +- `expiresInHours` optional +- default `24` +- max `168` (7 days) + +#### Response `201` + +```JSON +{ + "room": { + "id": "uuid", + "code": "ABC123", + "expiresAt": "2026-02-21T12:00:00.000Z" + }, + "member": { + "id": "uuid", + "role": "creator" + } +} +``` + +#### Errors + +- `500 INTERNAL_ERROR` if room code allocation fails (extremely unlikely, but handled) + +### POST /rooms/join + +Join an existing room by code. Creates a member and sets a session cookie. + +```JSON +Request body +{ + "code": "ABC123", + "displayName": "Ryan" +} +``` + +- `code` is case-insensitive +- `displayName` optional + +#### Response `200` + +```JSON +{ + "room": { + "id": "uuid", + "code": "ABC123", + "expiresAt": "2026-02-21T12:00:00.000Z" + }, + "member": { + "id": "uuid", + "role": "member", + "displayName": "Ryan" + }, + "session": { + "token": "raw-session-token" + } +} +``` + +> Note: A cookie is set even though the token is also returned in JSON. + +**Errors.** + +- `404 ROOM_NOT_FOUND`if code doesn’t exist +- `410 ROOM_GONE` if room was deleted or expired + +### GET /rooms/:roomId + +Get the current room state (room metadata + members list). + +**Auth required:** session cookie. + +#### URL params + +- `roomId`: UUID + +#### Response `200` + +```JSON +{ + "room": { + "id": "uuid", + "code": "ABC123", + "createdAt": "2026-02-20T12:00:00.000Z", + "expiresAt": "2026-02-21T12:00:00.000Z" + }, + "me": { + "memberId": "uuid", + "role": "creator" + }, + "members": [ + { + "id": "uuid", + "role": "creator", + "displayName": null, + "joinedAt": "2026-02-20T12:00:00.000Z", + "lastSeenAt": "2026-02-20T12:05:00.000Z" + }, + { + "id": "uuid", + "role": "member", + "displayName": "Ryan", + "joinedAt": "2026-02-20T12:02:00.000Z", + "lastSeenAt": "2026-02-20T12:06:00.000Z" + } + ] +} +``` + +The endpoint also updates the requester’s last_seen_at (best-effort presence tracking). + +#### Errors + +- `401 UNAUTHORISED` if missing/invalid/expired session +- `403 FORBIDDEN` if authenticated but not a member of the room +- `404 ROOM_NOT_FOUND` if roomId doesn’t exist +- `410 ROOM_GONE` if room deleted or expired + +### DELETE /rooms/:roomId + +Delete a room early. + +**Auth required:** session cookie. +**Authorization:** only the creator can delete. + +#### URL params + +- `roomId`: UUID + +#### Response `204` + +No content. + +#### Behavior notes + +Room is marked deleted (soft delete). + +Member sessions in the room are revoked. + +#### Errors + +- `401 UNAUTHORISED` if missing/invalid/expired session +- `403 FORBIDDEN` if not a member, or not creator +- `404 ROOM_NOT_FOUND` if roomId doesn’t exist +- `410 ROOM_GONE` if already deleted diff --git a/apps/backend/migrations/001_init.sql b/apps/backend/migrations/001_init.sql new file mode 100644 index 0000000..2b2549c --- /dev/null +++ b/apps/backend/migrations/001_init.sql @@ -0,0 +1,31 @@ +CREATE extension IF NOT EXISTS "pgcrypto"; + +CREATE TABLE IF NOT EXISTS rooms ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL, + deleted_at TIMESTAMPTZ NULL +); + +CREATE TABLE IF NOT EXISTS members ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + room_id uuid NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, + role TEXT NOT NULL CHECK (role IN ('creator', 'member')), + display_name TEXT NULL, + joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_seen_at TIMESTAMPTZ NULL +); + +CREATE INDEX IF NOT EXISTS idx_members_room_id ON members(room_id); + +CREATE TABLE IF NOT EXISTS member_sessions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + member_id uuid NOT NULL REFERENCES members(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL, + revoked_at TIMESTAMPTZ NULL +); + +CREATE INDEX IF NOT EXISTS idx_sessions_member_id ON member_sessions(member_id); \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index 09f4072..47c9e45 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -15,24 +15,31 @@ }, "dependencies": { "@gameswipe/shared": "*", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.3.1", - "express": "^4.19.2" + "express": "^4.22.1", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", + "pg": "^8.18.0", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/node": "^22.0.0", + "@types/express": "^4.17.25", + "@types/node": "^22.19.11", + "@types/pg": "^8.16.0", "@types/supertest": "^2.0.16", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "prettier": "^3.2.5", - "supertest": "^7.0.0", + "supertest": "^7.2.2", "tsx": "^4.0.0", "typescript": "^5.4.0", "typescript-eslint": "^8.0.0", - "vitest": "^2.0.0" + "vitest": "^2.1.9" } } diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts new file mode 100644 index 0000000..4a184d9 --- /dev/null +++ b/apps/backend/src/app.ts @@ -0,0 +1,31 @@ +import express from "express"; +import cookieParser from "cookie-parser"; +import helmet from "helmet"; +import rateLimit from "express-rate-limit"; +import { healthRouter } from "./routes/health"; +import { roomsRouter } from "./routes/rooms"; +import { errorHandler } from "./middleware/errorHandler"; + +export function createApp() { + const app = express(); + + app.use(helmet()); + app.use(express.json()); + app.use(cookieParser()); + + app.use( + "/rooms/join", + rateLimit({ + windowMs: 60_000, + limit: 30, + standardHeaders: true, + legacyHeaders: false + }) + ); + + app.use(healthRouter); + app.use(roomsRouter); + + app.use(errorHandler); + return app; +} diff --git a/apps/backend/src/auth/session.ts b/apps/backend/src/auth/session.ts new file mode 100644 index 0000000..17157fc --- /dev/null +++ b/apps/backend/src/auth/session.ts @@ -0,0 +1,10 @@ +import crypto from "crypto"; +import { env } from "../env"; + +export function newSessionToken(): string { + return crypto.randomBytes(32).toString("base64url"); +} + +export function hashToken(token: string): string { + return crypto.createHmac("sha256", env.SESSION_SECRET).update(token).digest("hex"); +} diff --git a/apps/backend/src/db.ts b/apps/backend/src/db.ts new file mode 100644 index 0000000..5a915bc --- /dev/null +++ b/apps/backend/src/db.ts @@ -0,0 +1,12 @@ +import { Pool } from "pg"; +import { env } from "./env"; + +const shouldUseSsl = + process.env.PGSSLMODE === "require" || + process.env.DATABASE_URL?.includes("sslmode=require") || + process.env.NODE_ENV === "production"; + +export const pool = new Pool({ + connectionString: env.DATABASE_URL, + ssl: shouldUseSsl ? { rejectUnauthorized: false } : false +}); diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 0c82bcc..7db7d97 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -1,6 +1,18 @@ import path from "node:path"; import dotenv from "dotenv"; +import { z } from "zod"; dotenv.config({ - path: path.resolve(process.cwd(), "../../.env") + path: path.resolve(__dirname, "../../..", ".env"), + override: true }); + +const EnvSchema = z.object({ + PORT: z.coerce.number().default(3000), + DATABASE_URL: z.string().min(1), + SESSION_SECRET: z.string().min(16), + SESSION_COOKIE_NAME: z.string().default("gs_session"), + SESSION_TTL_DAYS: z.coerce.number().default(30) +}); + +export const env = EnvSchema.parse(process.env); diff --git a/apps/backend/src/errors.ts b/apps/backend/src/errors.ts new file mode 100644 index 0000000..75d05d5 --- /dev/null +++ b/apps/backend/src/errors.ts @@ -0,0 +1,20 @@ +export type ApiErrorCode = + | "INVALID_REQUEST" + | "UNAUTHORISED" + | "FORBIDDEN" + | "ROOM_NOT_FOUND" + | "ROOM_GONE" + | "INTERNAL_ERROR"; + +export class ApiError extends Error { + public readonly status: number; + public readonly code: ApiErrorCode; + public readonly details?: unknown; + + constructor(status: number, code: ApiErrorCode, message: string, details?: unknown) { + super(message); + this.status = status; + this.code = code; + this.details = details; + } +} diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index b77f17c..3934eb9 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,22 +1,8 @@ -import express from "express"; -import cors from "cors"; -import "./env"; +import { createApp } from "./app"; +import { env } from "./env"; -const app = express(); +const app = createApp(); -app.use(cors()); -app.use(express.json()); - -app.get("/health", (_req, res) => { - res.status(200).json({ - ok: true, - service: "gameswipe-backend", - time: new Date().toISOString() - }); -}); - -const port = Number(process.env.PORT ?? 3000); - -app.listen(port, () => { - console.log(`Backend listening on http://localhost:${port}`); +app.listen(env.PORT, () => { + console.log(`Backend listening on http://localhost:${env.PORT}`); }); diff --git a/apps/backend/src/middleware/auth.ts b/apps/backend/src/middleware/auth.ts new file mode 100644 index 0000000..1656a96 --- /dev/null +++ b/apps/backend/src/middleware/auth.ts @@ -0,0 +1,43 @@ +import type { Request, RequestHandler } from "express"; +import { pool } from "../db"; +import { env } from "../env"; +import { ApiError } from "../errors"; +import { hashToken } from "../auth/session"; + +export type AuthedRequest = Request & { auth?: { memberId: string } }; + +type SessionRow = { member_id: string }; + +export const requireSession: RequestHandler = (req, _res, next) => { + void (async () => { + const cookies: Record = + (req as unknown as { cookies?: Record }).cookies ?? {}; + const token = cookies[env.SESSION_COOKIE_NAME]; + + if (typeof token !== "string" || token.length === 0) { + throw new ApiError(401, "UNAUTHORISED", "Missing session cookie"); + } + + const tokenHash = hashToken(token); + + const result = await pool.query( + ` + SELECT member_id + FROM member_sessions + WHERE token_hash = $1 + AND revoked_at IS NULL + AND expires_at > now() + `, + [tokenHash] + ); + + const row = result.rows[0]; + if (!row) { + throw new ApiError(401, "UNAUTHORISED", "Invalid or expired session"); + } + + (req as AuthedRequest).auth = { memberId: row.member_id }; + })() + .then(() => next()) + .catch(next); +}; diff --git a/apps/backend/src/middleware/errorHandler.ts b/apps/backend/src/middleware/errorHandler.ts new file mode 100644 index 0000000..8273734 --- /dev/null +++ b/apps/backend/src/middleware/errorHandler.ts @@ -0,0 +1,23 @@ +import { Request, Response, NextFunction } from "express"; +import { ApiError } from "../errors"; + +export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) { + if (err instanceof ApiError) { + res.status(err.status).json({ + error: { + code: err.code, + message: err.message, + details: err.details ?? null + } + }); + return; + } + + res.status(500).json({ + error: { + code: "INTERNAL_ERROR", + message: "Unexpected Server Error", + details: null + } + }); +} diff --git a/apps/backend/src/routes/health.ts b/apps/backend/src/routes/health.ts new file mode 100644 index 0000000..1f8b9f5 --- /dev/null +++ b/apps/backend/src/routes/health.ts @@ -0,0 +1,7 @@ +import { Router } from "express"; + +export const healthRouter = Router(); + +healthRouter.get("/health", (_req, res) => { + res.json({ ok: true }); +}); diff --git a/apps/backend/src/routes/rooms.ts b/apps/backend/src/routes/rooms.ts new file mode 100644 index 0000000..cc6fd0d --- /dev/null +++ b/apps/backend/src/routes/rooms.ts @@ -0,0 +1,356 @@ +import { Router, type Response } from "express"; +import { z } from "zod"; +import { pool } from "../db"; +import { ApiError } from "../errors"; +import { generateRoomCode } from "../utils/roomCode"; +import { env } from "../env"; +import { newSessionToken, hashToken } from "../auth/session"; +import { requireSession, type AuthedRequest } from "../middleware/auth"; +import { asyncHandler } from "../utils/asyncHandler"; +import type { PoolClient } from "pg"; + +export const roomsRouter = Router(); + +const CreateRoomSchema = z.object({ + expiresInHours: z.number().int().positive().max(168).optional() +}); + +const JoinRoomSchema = z.object({ + code: z.string().min(3).max(32), + displayName: z.string().min(1).max(48).optional() +}); + +const RoomIdParamSchema = z.object({ + roomId: z.string().uuid() +}); + +function setSessionCookie(res: Response, token: string): void { + res.cookie(env.SESSION_COOKIE_NAME, token, { + httpOnly: true, + sameSite: "lax", + secure: false, + maxAge: env.SESSION_TTL_DAYS * 24 * 60 * 60 * 1000 + }); +} + +type RoomRow = { + id: string; + code: string; + expires_at: string; + deleted_at: string | null; + created_at?: string; +}; + +type MemberRole = "creator" | "member"; + +type MemberRow = { + id: string; + role: MemberRole; + display_name: string | null; + joined_at?: string; + last_seen_at?: string | null; +}; + +type SessionInsertRow = { id: string }; + +function isPgErrorWithCode(err: unknown): err is { code: string } { + if (typeof err !== "object" || err === null) return false; + const rec = err as Record; + return typeof rec.code === "string"; +} + +async function createSession(client: PoolClient, memberId: string): Promise { + const token = newSessionToken(); + const tokenHash = hashToken(token); + const expiresAt = new Date(Date.now() + env.SESSION_TTL_DAYS * 24 * 60 * 60 * 1000); + + const result = await client.query( + ` + INSERT INTO member_sessions (member_id, token_hash, expires_at) + VALUES ($1, $2, $3) + RETURNING id + `, + [memberId, tokenHash, expiresAt.toISOString()] + ); + + const row = result.rows[0]; + if (!row) throw new ApiError(500, "INTERNAL_ERROR", "Session insert did not return a row"); + + return token; +} + +function requireAuthedMemberId(req: AuthedRequest): string { + const memberId = req.auth?.memberId; + if (!memberId) throw new ApiError(401, "UNAUTHORISED", "Missing auth context"); + return memberId; +} + +function ensureRoomActive(room: RoomRow): void { + if (room.deleted_at !== null) throw new ApiError(410, "ROOM_GONE", "Room has been deleted"); + if (new Date(room.expires_at).getTime() <= Date.now()) throw new ApiError(410, "ROOM_GONE", "Room has expired"); +} + +roomsRouter.post( + "/rooms", + asyncHandler(async (req, res) => { + const body = CreateRoomSchema.parse(req.body ?? {}); + const hours = body.expiresInHours ?? 24; + const expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000); + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + let roomRow: Pick | null = null; + + for (let attempt = 0; attempt < 8; attempt++) { + const code = generateRoomCode(6); + try { + const insertedRoom = await client.query>( + ` + INSERT INTO rooms (code, expires_at) + VALUES ($1, $2) + RETURNING id, code, expires_at + `, + [code, expiresAt.toISOString()] + ); + + const row = insertedRoom.rows[0]; + if (!row) throw new ApiError(500, "INTERNAL_ERROR", "Room insert did not return a row"); + + roomRow = row; + break; + } catch (e: unknown) { + // 23505 = unique_violation + if (isPgErrorWithCode(e) && e.code === "23505") continue; + throw e; + } + } + + if (!roomRow) throw new ApiError(500, "INTERNAL_ERROR", "Failed to allocate unique room code"); + + const insertedMember = await client.query>( + ` + INSERT INTO members (room_id, role) + VALUES ($1, 'creator') + RETURNING id, role + `, + [roomRow.id] + ); + + const memberRow = insertedMember.rows[0]; + if (!memberRow) throw new ApiError(500, "INTERNAL_ERROR", "Member insert did not return a row"); + + const token = await createSession(client, memberRow.id); + setSessionCookie(res, token); + + await client.query("COMMIT"); + + res.status(201).json({ + room: { id: roomRow.id, code: roomRow.code, expiresAt: roomRow.expires_at }, + member: { id: memberRow.id, role: memberRow.role } + }); + } catch (e: unknown) { + try { + await client.query("ROLLBACK"); + } catch { + // ignore rollback errors + } + throw e; + } finally { + client.release(); + } + }) +); + +roomsRouter.post( + "/rooms/join", + asyncHandler(async (req, res) => { + const body = JoinRoomSchema.parse(req.body ?? {}); + const code = body.code.toUpperCase(); + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const roomResult = await client.query( + ` + SELECT id, code, expires_at, deleted_at + FROM rooms + WHERE code = $1 + `, + [code] + ); + + const roomRow = roomResult.rows[0]; + if (!roomRow) throw new ApiError(404, "ROOM_NOT_FOUND", "Room code not found"); + + ensureRoomActive(roomRow); + + const memberInsert = await client.query( + ` + INSERT INTO members (room_id, role, display_name) + VALUES ($1, 'member', $2) + RETURNING id, role, display_name + `, + [roomRow.id, body.displayName ?? null] + ); + + const memberRow = memberInsert.rows[0]; + if (!memberRow) throw new ApiError(500, "INTERNAL_ERROR", "Member insert did not return a row"); + + const token = await createSession(client, memberRow.id); + setSessionCookie(res, token); + + await client.query("COMMIT"); + + res.json({ + room: { id: roomRow.id, code: roomRow.code, expiresAt: roomRow.expires_at }, + member: { id: memberRow.id, role: memberRow.role, displayName: memberRow.display_name }, + session: { token } + }); + } catch (e: unknown) { + try { + await client.query("ROLLBACK"); + } catch { + // ignore rollback errors + } + throw e; + } finally { + client.release(); + } + }) +); + +roomsRouter.get( + "/rooms/:roomId", + requireSession, + asyncHandler(async (req: AuthedRequest, res) => { + const { roomId } = RoomIdParamSchema.parse(req.params); + const memberId = requireAuthedMemberId(req); + + const meResult = await pool.query>( + ` + SELECT id, role + FROM members + WHERE id = $1 AND room_id = $2 + `, + [memberId, roomId] + ); + + const meRow = meResult.rows[0]; + if (!meRow) throw new ApiError(403, "FORBIDDEN", "Not a member of this room"); + + const roomResult = await pool.query>( + ` + SELECT id, code, created_at, expires_at, deleted_at + FROM rooms + WHERE id = $1 + `, + [roomId] + ); + + const roomRow = roomResult.rows[0]; + if (!roomRow) throw new ApiError(404, "ROOM_NOT_FOUND", "Room not found"); + + ensureRoomActive(roomRow); + + await pool.query(`UPDATE members SET last_seen_at = now() WHERE id = $1`, [memberId]); + + const membersResult = await pool.query< + Pick + >( + ` + SELECT id, role, display_name, joined_at, last_seen_at + FROM members + WHERE room_id = $1 + ORDER BY joined_at ASC + `, + [roomId] + ); + + res.json({ + room: { + id: roomRow.id, + code: roomRow.code, + createdAt: roomRow.created_at ?? null, + expiresAt: roomRow.expires_at + }, + me: { memberId: meRow.id, role: meRow.role }, + members: membersResult.rows.map((m) => ({ + id: m.id, + role: m.role, + displayName: m.display_name, + joinedAt: m.joined_at ?? null, + lastSeenAt: m.last_seen_at ?? null + })) + }); + }) +); + +roomsRouter.delete( + "/rooms/:roomId", + requireSession, + asyncHandler(async (req: AuthedRequest, res) => { + const { roomId } = RoomIdParamSchema.parse(req.params); + const memberId = requireAuthedMemberId(req); + + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const meResult = await client.query>( + ` + SELECT role + FROM members + WHERE id = $1 AND room_id = $2 + `, + [memberId, roomId] + ); + + const meRow = meResult.rows[0]; + if (!meRow) throw new ApiError(403, "FORBIDDEN", "Not a member of this room"); + if (meRow.role !== "creator") throw new ApiError(403, "FORBIDDEN", "Only the creator can delete the room"); + + const roomResult = await client.query>( + ` + SELECT deleted_at, expires_at + FROM rooms + WHERE id = $1 + `, + [roomId] + ); + + const roomRow = roomResult.rows[0]; + if (!roomRow) throw new ApiError(404, "ROOM_NOT_FOUND", "Room not found"); + if (roomRow.deleted_at !== null) throw new ApiError(410, "ROOM_GONE", "Room has already been deleted"); + + await client.query(`UPDATE rooms SET deleted_at = now() WHERE id = $1`, [roomId]); + + await client.query( + ` + UPDATE member_sessions ms + SET revoked_at = now() + FROM members m + WHERE m.id = ms.member_id + AND m.room_id = $1 + AND ms.revoked_at IS NULL + AND ms.member_id <> $2 + `, + [roomId, memberId] + ); + + await client.query("COMMIT"); + res.status(204).send(); + } catch (e: unknown) { + try { + await client.query("ROLLBACK"); + } catch { + // ignore rollback errors + } + throw e; + } finally { + client.release(); + } + }) +); diff --git a/apps/backend/src/utils/asyncHandler.ts b/apps/backend/src/utils/asyncHandler.ts new file mode 100644 index 0000000..8ab34ad --- /dev/null +++ b/apps/backend/src/utils/asyncHandler.ts @@ -0,0 +1,9 @@ +import type { Request, Response, NextFunction, RequestHandler } from "express"; + +export function asyncHandler( + fn: (req: Request, res: Response, next: NextFunction) => Promise +): RequestHandler { + return (req, res, next) => { + void fn(req, res, next).catch(next); + }; +} diff --git a/apps/backend/src/utils/roomCode.ts b/apps/backend/src/utils/roomCode.ts new file mode 100644 index 0000000..5d8e141 --- /dev/null +++ b/apps/backend/src/utils/roomCode.ts @@ -0,0 +1,14 @@ +import crypto from "crypto"; + +const ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +export function generateRoomCode(length = 6): string { + const bytes = crypto.randomBytes(length); + let out = ""; + + for (const byte of bytes) { + out += ALPHABET[byte % ALPHABET.length]; + } + + return out; +} diff --git a/apps/backend/test/db.ts b/apps/backend/test/db.ts new file mode 100644 index 0000000..2d81c3d --- /dev/null +++ b/apps/backend/test/db.ts @@ -0,0 +1,16 @@ +import { pool } from "../src/db"; + +export async function resetDb(): Promise { + await pool.query(` + TRUNCATE TABLE + member_sessions, + members, + rooms + RESTART IDENTITY + CASCADE + `); +} + +export async function closeDb(): Promise { + await pool.end(); +} diff --git a/apps/backend/src/health.test.ts b/apps/backend/test/health.test.ts similarity index 100% rename from apps/backend/src/health.test.ts rename to apps/backend/test/health.test.ts diff --git a/apps/backend/test/rooms.test.ts b/apps/backend/test/rooms.test.ts new file mode 100644 index 0000000..fad9f32 --- /dev/null +++ b/apps/backend/test/rooms.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, beforeEach, afterAll } from "vitest"; +import request from "supertest"; +import { createApp } from "../src/app"; +import { resetDb, closeDb } from "./db"; +import { pool } from "../src/db"; + +type CreateRoomResponse = { + room: { id: string; code: string; expiresAt: string }; + member: { id: string; role: "creator" | "member" }; +}; + +type JoinRoomResponse = { + room: { id: string; code: string; expiresAt: string }; + member: { id: string; role: "creator" | "member"; displayName: string | null }; + session?: { token: string }; +}; + +type GetRoomResponse = { + room: { id: string; code: string; createdAt: string | null; expiresAt: string }; + me: { memberId: string; role: "creator" | "member" }; + members: Array<{ + id: string; + role: "creator" | "member"; + displayName: string | null; + joinedAt: string | null; + lastSeenAt: string | null; + }>; +}; + +function assertCreateRoomBody(body: unknown): asserts body is CreateRoomResponse { + if (typeof body !== "object" || body === null) throw new Error("Expected object body"); + const rec = body as Record; + if (typeof rec.room !== "object" || rec.room === null) throw new Error("Missing room"); + if (typeof rec.member !== "object" || rec.member === null) throw new Error("Missing member"); +} + +function assertJoinRoomBody(body: unknown): asserts body is JoinRoomResponse { + if (typeof body !== "object" || body === null) throw new Error("Expected object body"); + const rec = body as Record; + if (typeof rec.room !== "object" || rec.room === null) throw new Error("Missing room"); + if (typeof rec.member !== "object" || rec.member === null) throw new Error("Missing member"); +} + +function assertGetRoomBody(body: unknown): asserts body is GetRoomResponse { + if (typeof body !== "object" || body === null) throw new Error("Expected object body"); + const rec = body as Record; + if (typeof rec.room !== "object" || rec.room === null) throw new Error("Missing room"); + if (typeof rec.me !== "object" || rec.me === null) throw new Error("Missing me"); + if (!Array.isArray(rec.members)) throw new Error("Missing members array"); +} + +function assertErrorBody(body: unknown): asserts body is { error?: { code: string } } { + if (typeof body !== "object" || body === null) throw new Error("Expected object body"); +} + +describe("Rooms API", () => { + const app = createApp(); + + beforeEach(async () => { + await resetDb(); + }); + + afterAll(async () => { + await closeDb(); + }); + + it("POST /rooms creates a room + creator session cookie", async () => { + const agent = request.agent(app); + + const res = await agent.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + + expect(res.status).toBe(201); + assertCreateRoomBody(res.body); + + expect(typeof res.body.room.id).toBe("string"); + expect(typeof res.body.room.code).toBe("string"); + expect(res.body.member.role).toBe("creator"); + + const setCookieHeader = res.headers["set-cookie"]; + const cookies = Array.isArray(setCookieHeader) + ? setCookieHeader + : typeof setCookieHeader === "string" + ? [setCookieHeader] + : []; + + expect(cookies.join(";")).toContain("gs_session="); + }); + + it("POST /rooms/join returns 404 for unknown code", async () => { + const res = await request(app) + .post("/rooms/join") + .send({ code: "ABC234", displayName: "Ryan" }) + .set("Content-Type", "application/json"); + + expect(res.status).toBe(404); + assertErrorBody(res.body); + expect(res.body?.error?.code).toBe("ROOM_NOT_FOUND"); + }); + + it("POST /rooms/join joins an existing room and sets cookie", async () => { + const creator = request.agent(app); + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + + const code = createRes.body.room.code; + + const joiner = request.agent(app); + const joinRes = await joiner + .post("/rooms/join") + .send({ code, displayName: "Ryan" }) + .set("Content-Type", "application/json"); + + expect(joinRes.status).toBe(200); + assertJoinRoomBody(joinRes.body); + + expect(joinRes.body.room.code).toBe(code); + expect(joinRes.body.member.role).toBe("member"); + expect(joinRes.body.member.displayName).toBe("Ryan"); + + const setCookieHeader = joinRes.headers["set-cookie"]; + const cookies = Array.isArray(setCookieHeader) + ? setCookieHeader + : typeof setCookieHeader === "string" + ? [setCookieHeader] + : []; + + expect(cookies.join(";")).toContain("gs_session="); + }); + + it("GET /rooms/:roomId requires session", async () => { + const res = await request(app).get("/rooms/00000000-0000-0000-0000-000000000000"); + expect(res.status).toBe(401); + assertErrorBody(res.body); + expect(res.body?.error?.code).toBe("UNAUTHORISED"); + }); + + it("GET /rooms/:roomId returns room + member list for an authenticated member", async () => { + const creator = request.agent(app); + + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + + const roomId = createRes.body.room.id; + const code = createRes.body.room.code; + + // Join a second member + const joiner = request.agent(app); + const joinRes = await joiner + .post("/rooms/join") + .send({ code, displayName: "Ryan" }) + .set("Content-Type", "application/json"); + expect(joinRes.status).toBe(200); + + // Creator fetches room + const res = await creator.get(`/rooms/${roomId}`); + expect(res.status).toBe(200); + assertGetRoomBody(res.body); + + expect(res.body.room.id).toBe(roomId); + expect(res.body.room.code).toBe(code); + expect(res.body.members.length).toBe(2); + + const roles = res.body.members.map((m) => m.role).sort(); + expect(roles).toEqual(["creator", "member"]); + }); + + it("GET /rooms/:roomId returns 410 when room expired", async () => { + const creator = request.agent(app); + + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + const roomId = createRes.body.room.id; + + // Force expire in DB + await pool.query(`UPDATE rooms SET expires_at = now() - interval '1 minute' WHERE id = $1`, [roomId]); + + const res = await creator.get(`/rooms/${roomId}`); + expect(res.status).toBe(410); + assertErrorBody(res.body); + expect(res.body?.error?.code).toBe("ROOM_GONE"); + }); + + it("DELETE /rooms/:roomId requires creator role", async () => { + const creator = request.agent(app); + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + + const roomId = createRes.body.room.id; + const code = createRes.body.room.code; + + const joiner = request.agent(app); + const joinRes = await joiner + .post("/rooms/join") + .send({ code, displayName: "Ryan" }) + .set("Content-Type", "application/json"); + expect(joinRes.status).toBe(200); + + const delRes = await joiner.delete(`/rooms/${roomId}`); + expect(delRes.status).toBe(403); + assertErrorBody(delRes.body); + expect(delRes.body?.error?.code).toBe("FORBIDDEN"); + }); + + it("DELETE /rooms/:roomId deletes room and revokes sessions", async () => { + const creator = request.agent(app); + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + + const roomId = createRes.body.room.id; + const code = createRes.body.room.code; + + const joiner = request.agent(app); + const joinRes = await joiner + .post("/rooms/join") + .send({ code, displayName: "Ryan" }) + .set("Content-Type", "application/json"); + expect(joinRes.status).toBe(200); + + const delRes = await creator.delete(`/rooms/${roomId}`); + expect(delRes.status).toBe(204); + + // After deletion, room fetch should be 410 for creator + const resCreator = await creator.get(`/rooms/${roomId}`); + expect(resCreator.status).toBe(410); + assertErrorBody(resCreator.body); + expect(resCreator.body?.error?.code).toBe("ROOM_GONE"); + + // Joiner session should now be revoked; should get 401 on protected endpoint + const resJoiner = await joiner.get(`/rooms/${roomId}`); + expect(resJoiner.status).toBe(401); + assertErrorBody(resJoiner.body); + expect(resJoiner.body?.error?.code).toBe("UNAUTHORISED"); + }); + + it("POST /rooms/join returns 410 if room deleted", async () => { + const creator = request.agent(app); + const createRes = await creator.post("/rooms").send({ expiresInHours: 24 }).set("Content-Type", "application/json"); + expect(createRes.status).toBe(201); + assertCreateRoomBody(createRes.body); + + const roomId = createRes.body.room.id; + const code = createRes.body.room.code; + + const delRes = await creator.delete(`/rooms/${roomId}`); + expect(delRes.status).toBe(204); + + const joinRes = await request(app) + .post("/rooms/join") + .send({ code, displayName: "Ryan" }) + .set("Content-Type", "application/json"); + + expect(joinRes.status).toBe(410); + assertErrorBody(joinRes.body); + expect(joinRes.body?.error?.code).toBe("ROOM_GONE"); + }); +}); diff --git a/apps/backend/test/setup.ts b/apps/backend/test/setup.ts new file mode 100644 index 0000000..f44f830 --- /dev/null +++ b/apps/backend/test/setup.ts @@ -0,0 +1,17 @@ +import path from "node:path"; +import dotenv from "dotenv"; +import { beforeAll } from "vitest"; + +dotenv.config({ + path: path.resolve(__dirname, "../../..", ".env"), + override: true +}); + +beforeAll(() => { + const testUrl = process.env.DATABASE_URL_TEST; + if (typeof testUrl !== "string" || testUrl.length === 0) { + throw new Error("DATABASE_URL_TEST is missing. Add DATABASE_URL_TEST to the repo root .env"); + } + + process.env.DATABASE_URL = testUrl; +}); diff --git a/apps/backend/tsconfig.test.json b/apps/backend/tsconfig.test.json new file mode 100644 index 0000000..ae64a0e --- /dev/null +++ b/apps/backend/tsconfig.test.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", "vitest.config.ts"] +} diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts index 99f4bce..6926c76 100644 --- a/apps/backend/vitest.config.ts +++ b/apps/backend/vitest.config.ts @@ -2,6 +2,10 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { - environment: "node" + environment: "node", + setupFiles: ["./test/setup.ts"], + globals: false, + testTimeout: 30_000, + hookTimeout: 30_000 } }); diff --git a/apps/frontend/README.md b/apps/frontend/README.md index d2cc24c..29282f7 100644 --- a/apps/frontend/README.md +++ b/apps/frontend/README.md @@ -1,73 +1,57 @@ -# React + TypeScript + Vite +# GameSwipe Frontend (apps/frontend) -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +Vite + React + TypeScript frontend for GameSwipe (room-based multiplayer game picker). -Currently, two official plugins are available: +## Tech stack -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh +- React + TypeScript (Vite) +- ESLint via the repo root config (`eslint.config.mjs`) -## React Compiler +## Run locally -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +### Prerequisites -## Expanding the ESLint configuration +- Node.js (same version as repo standard) +- Backend running locally (for any API calls the UI makes) -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: +### Install dependencies (from repo root) -```js -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... +```bash +npm install +``` + +### Start frontend dev server (from repo root) + +```BASH +npm --workspace apps/frontend run dev +``` + +Vite will print the local URL (typically something like ). - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked +### Environment variables - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname - } - // other options... - } - } -]); +These are read from the repo root `.env` (recommended): + +```env +FRONTEND_PORT=5173 +VITE_API_BASE_URL= ``` -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from "eslint-plugin-react-x"; -import reactDom from "eslint-plugin-react-dom"; - -export default defineConfig([ - globalIgnores(["dist"]), - { - files: ["**/*.{ts,tsx}"], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs["recommended-typescript"], - // Enable lint rules for React DOM - reactDom.configs.recommended - ], - languageOptions: { - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname - } - // other options... - } - } -]); +### Scripts + +Run from the repo root: + +```BASH +npm --workspace apps/frontend run dev +npm --workspace apps/frontend run build +npm --workspace apps/frontend run preview +npm --workspace apps/frontend run lint ``` + +### Project structure + +- `src/pages/` - route-level pages (Room create/join, Swipe, Results) +- `src/components/` - reusable UI components +- `src/api/` - typed API client + DTOs (prefer shared types from packages/shared) +- `src/state/` - client state + server cache (if you adopt React Query or similar) +- `src/styles/` - global styles / theme tokens diff --git a/eslint.config.mjs b/eslint.config.mjs index 4bf0c4d..4760e0d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,6 +12,7 @@ const repoRoot = __dirname; const tsProjects = [ path.join(repoRoot, "apps/backend/tsconfig.json"), + path.join(repoRoot, "apps/backend/tsconfig.test.json"), path.join(repoRoot, "apps/frontend/tsconfig.app.json"), path.join(repoRoot, "packages/shared/tsconfig.json") ]; @@ -66,5 +67,19 @@ export default [ ...reactHooks.configs.recommended.rules, "react-refresh/only-export-components": ["warn", { allowConstantExport: true }] } + }, + + { + files: ["**/*.{ts,tsx}"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_" + } + ] + } } ]; diff --git a/package-lock.json b/package-lock.json index 522e3df..b7bb8aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,10 @@ ], "devDependencies": { "@eslint/js": "^9.39.2", + "@types/pg": "^8.16.0", "concurrently": "^9.2.1", "eslint": "^9.39.2", + "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.0", @@ -29,76 +31,32 @@ "version": "1.0.0", "dependencies": { "@gameswipe/shared": "*", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^17.3.1", - "express": "^4.19.2" + "express": "^4.22.1", + "express-rate-limit": "^8.2.1", + "helmet": "^8.1.0", + "pg": "^8.18.0", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/js": "^9.0.0", + "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/node": "^22.0.0", + "@types/express": "^4.17.25", + "@types/node": "^22.19.11", + "@types/pg": "^8.16.0", "@types/supertest": "^2.0.16", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "prettier": "^3.2.5", - "supertest": "^7.0.0", + "supertest": "^7.2.2", "tsx": "^4.0.0", "typescript": "^5.4.0", "typescript-eslint": "^8.0.0", - "vitest": "^2.0.0" - } - }, - "apps/backend/node_modules/accepts": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "apps/backend/node_modules/body-parser": { - "version": "2.2.2", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/content-disposition": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/cookie-signature": { - "version": "1.2.2", - "license": "MIT", - "engines": { - "node": ">=6.6.0" + "vitest": "^2.1.9" } }, "apps/backend/node_modules/cors": { @@ -116,237 +74,6 @@ "url": "https://opencollective.com/express" } }, - "apps/backend/node_modules/express": { - "version": "5.2.1", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/finalhandler": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/fresh": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "apps/backend/node_modules/iconv-lite": { - "version": "0.7.2", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/is-promise": { - "version": "4.0.0", - "license": "MIT" - }, - "apps/backend/node_modules/media-typer": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "apps/backend/node_modules/merge-descriptors": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "apps/backend/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/backend/node_modules/mime-types": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/negotiator": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "apps/backend/node_modules/path-to-regexp": { - "version": "8.3.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/qs": { - "version": "6.15.0", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "apps/backend/node_modules/raw-body": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "apps/backend/node_modules/router": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "apps/backend/node_modules/send": { - "version": "1.2.1", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/serve-static": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "apps/backend/node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "apps/frontend": { "name": "@gameswipe/frontend", "version": "0.1.0", @@ -2056,6 +1783,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -2086,6 +1823,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -2127,6 +1865,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -2151,6 +1899,18 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2251,6 +2011,13 @@ "@types/superagent": "*" } }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.56.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", @@ -2668,6 +2435,19 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2779,6 +2559,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -2992,6 +2778,45 @@ "node": ">=6.0.0" } }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -3183,6 +3008,39 @@ "node": ">=8" } }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", @@ -3283,6 +3141,18 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", @@ -3308,6 +3178,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", @@ -3437,6 +3326,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3539,6 +3429,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -4058,6 +3958,23 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-markdown": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-5.1.0.tgz", + "integrity": "sha512-SJeyKko1K6GwI0AN6xeCDToXDkfKZfXcexA6B+O2Wr2btUS9GrC+YgwSyVli5DJnctUHjFXcQ2cqTaAmVoLi2A==", + "deprecated": "Please use @eslint/markdown instead", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^0.8.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -4268,6 +4185,85 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4327,6 +4323,39 @@ "node": ">=16.0.0" } }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4425,6 +4454,15 @@ "node": ">= 0.6" } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4725,6 +4763,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -4803,6 +4850,18 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4871,6 +4930,15 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "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", @@ -4880,6 +4948,32 @@ "node": ">= 0.10" } }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5015,6 +5109,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5084,6 +5189,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -5539,21 +5655,99 @@ "node": ">= 0.4" } }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5563,7 +5757,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5637,6 +5830,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-exports-info": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", @@ -5807,6 +6009,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5893,6 +6096,25 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5942,6 +6164,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", @@ -5959,6 +6187,96 @@ "node": ">= 14.16" } }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6019,6 +6337,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6132,7 +6489,6 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -6160,6 +6516,21 @@ "node": ">= 0.6" } }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -6402,6 +6773,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -6475,6 +6866,60 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -6655,6 +7100,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -7137,6 +7591,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -7280,6 +7747,20 @@ "dev": true, "license": "MIT" }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -7351,6 +7832,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8168,6 +8658,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -8209,6 +8700,15 @@ "dev": true, "license": "MIT" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 4034711..6ca1151 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "homepage": "https://github.com/Coding-Kitties-Club/GameSwipe#readme", "devDependencies": { "@eslint/js": "^9.39.2", + "@types/pg": "^8.16.0", "concurrently": "^9.2.1", "eslint": "^9.39.2", "eslint-plugin-react": "^7.37.5",