diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 85cd7eb..da60d5f 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,21 +4,15 @@ - - + - - - - - - - + + @@ -72,6 +66,11 @@ "to.speed.mode.migration.done": "true", "ts.external.directory.path": "/home/micosil/WebstormProjects/desker/node_modules/typescript/lib", "vue.rearranger.settings.migration": "true" + }, + "keyToStringList": { + "DatabaseDriversLRU": [ + "postgresql" + ] } }]]> @@ -130,7 +129,7 @@ 1774643312599 - + @@ -148,7 +147,15 @@ 1774653909300 - + + + 1774654565964 + + + + 1774654565964 + + @@ -157,6 +164,7 @@ - + + \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 91040f1..c805cd9 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -26,6 +26,8 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^10.4.22", + "@nestjs/platform-socket.io": "^10.4.22", + "@nestjs/websockets": "^10.4.22", "@prisma/client": "^6.4.0", "@types/bcrypt": "^6.0.0", "@types/passport-jwt": "^4.0.1", @@ -36,7 +38,8 @@ "passport-jwt": "^4.0.1", "prisma": "^6.4.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.2" + "rxjs": "^7.8.2", + "socket.io": "^4.8.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index fca9a41..47e0bbf 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -50,24 +50,38 @@ model Floor { } model Space { - id String @id @default(uuid()) - name String - type SpaceType - floor Floor @relation(fields: [floorId], references: [id]) - floorId String - x Float? // Percentage-based for responsiveness (Stage 3) - y Float? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + name String + type SpaceType + floor Floor @relation(fields: [floorId], references: [id]) + floorId String + x Float? // Percentage-based for responsiveness (Stage 3) + y Float? + reservations Reservation[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model User { - id String @id @default(uuid()) - email String @unique + id String @id @default(uuid()) + email String @unique password String - role Role @default(EMPLOYEE) - organization Organization @relation(fields: [organizationId], references: [id]) + role Role @default(EMPLOYEE) + organization Organization @relation(fields: [organizationId], references: [id]) organizationId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + reservations Reservation[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Reservation { + id String @id @default(uuid()) + startTime DateTime + endTime DateTime + user User @relation(fields: [userId], references: [id]) + userId String + space Space @relation(fields: [spaceId], references: [id]) + spaceId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 53d75f5..343b905 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,10 +1,13 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PrismaModule } from './prisma/prisma.module'; import { SpacesModule } from './spaces/spaces.module'; import { AuthModule } from './auth/auth.module'; +import { ReservationsModule } from './reservations/reservations.module'; +import { EventsModule } from './events/events.module'; +import { LoggerMiddleware } from './common/middleware/logger.middleware'; @Module({ imports: [ @@ -12,8 +15,14 @@ import { AuthModule } from './auth/auth.module'; PrismaModule, SpacesModule, AuthModule, + ReservationsModule, + EventsModule, ], controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(LoggerMiddleware).forRoutes('*'); + } +} diff --git a/apps/api/src/common/filters/http-exception.filter.ts b/apps/api/src/common/filters/http-exception.filter.ts index 0def6e0..861999a 100644 --- a/apps/api/src/common/filters/http-exception.filter.ts +++ b/apps/api/src/common/filters/http-exception.filter.ts @@ -20,6 +20,10 @@ export class HttpExceptionFilter implements ExceptionFilter { ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; + if (!isHttpException) { + console.error(exception); + } + const message = isHttpException ? exception.getResponse() : 'Internal server error'; diff --git a/apps/api/src/common/middleware/logger.middleware.ts b/apps/api/src/common/middleware/logger.middleware.ts new file mode 100644 index 0000000..50286ec --- /dev/null +++ b/apps/api/src/common/middleware/logger.middleware.ts @@ -0,0 +1,23 @@ +import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +@Injectable() +export class LoggerMiddleware implements NestMiddleware { + private logger = new Logger('HTTP'); + + use(request: Request, response: Response, next: NextFunction): void { + const { method, originalUrl } = request; + const userAgent = request.get('user-agent') || ''; + + response.on('finish', () => { + const { statusCode } = response; + const contentLength = response.get('content-length'); + + this.logger.log( + `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent}`, + ); + }); + + next(); + } +} diff --git a/apps/api/src/events/events.gateway.ts b/apps/api/src/events/events.gateway.ts new file mode 100644 index 0000000..13e39bf --- /dev/null +++ b/apps/api/src/events/events.gateway.ts @@ -0,0 +1,45 @@ +import { + WebSocketGateway, + WebSocketServer, + SubscribeMessage, + OnGatewayConnection, + OnGatewayDisconnect, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; + +@WebSocketGateway({ + cors: { + origin: '*', + }, +}) +export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() + server: Server; + + handleConnection(client: Socket) { + console.log(`Client connected: ${client.id}`); + } + + handleDisconnect(client: Socket) { + console.log(`Client disconnected: ${client.id}`); + } + + @SubscribeMessage('join_floor') + handleJoinFloor(client: Socket, floorId: string) { + client.join(floorId); + console.log(`Client ${client.id} joined floor: ${floorId}`); + } + + @SubscribeMessage('leave_floor') + handleLeaveFloor(client: Socket, floorId: string) { + client.leave(floorId); + console.log(`Client ${client.id} left floor: ${floorId}`); + } + + emitSpaceStatusChanged(floorId: string, spaceId: string, isOccupied: boolean) { + this.server.to(floorId).emit('space_status_changed', { + spaceId, + isOccupied, + }); + } +} diff --git a/apps/api/src/events/events.module.ts b/apps/api/src/events/events.module.ts new file mode 100644 index 0000000..d97b523 --- /dev/null +++ b/apps/api/src/events/events.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { EventsGateway } from './events.gateway'; + +@Module({ + providers: [EventsGateway], + exports: [EventsGateway], +}) +export class EventsModule {} diff --git a/apps/api/src/reservations/dto/create-reservation.dto.ts b/apps/api/src/reservations/dto/create-reservation.dto.ts new file mode 100644 index 0000000..d8e2aa4 --- /dev/null +++ b/apps/api/src/reservations/dto/create-reservation.dto.ts @@ -0,0 +1,15 @@ +import { IsString, IsDateString, IsNotEmpty } from 'class-validator'; + +export class CreateReservationDto { + @IsDateString() + @IsNotEmpty() + startTime: string; + + @IsDateString() + @IsNotEmpty() + endTime: string; + + @IsString() + @IsNotEmpty() + spaceId: string; +} diff --git a/apps/api/src/reservations/reservations.controller.ts b/apps/api/src/reservations/reservations.controller.ts new file mode 100644 index 0000000..4f9665d --- /dev/null +++ b/apps/api/src/reservations/reservations.controller.ts @@ -0,0 +1,34 @@ +import { + Controller, + Post, + Delete, + Get, + Body, + Param, + UseGuards, + Req, +} from '@nestjs/common'; +import { ReservationsService } from './reservations.service'; +import { CreateReservationDto } from './dto/create-reservation.dto'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; + +@Controller('reservations') +@UseGuards(JwtAuthGuard) +export class ReservationsController { + constructor(private readonly reservationsService: ReservationsService) {} + + @Post() + create(@Req() req, @Body() createReservationDto: CreateReservationDto) { + return this.reservationsService.create(req.user.id, createReservationDto); + } + + @Delete(':id') + cancel(@Param('id') id: string) { + return this.reservationsService.cancel(id); + } + + @Get('my') + findAllMy(@Req() req) { + return this.reservationsService.findAllByUserId(req.user.id); + } +} diff --git a/apps/api/src/reservations/reservations.module.ts b/apps/api/src/reservations/reservations.module.ts new file mode 100644 index 0000000..b30889d --- /dev/null +++ b/apps/api/src/reservations/reservations.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ReservationsService } from './reservations.service'; +import { ReservationsController } from './reservations.controller'; +import { PrismaModule } from '../prisma/prisma.module'; +import { EventsModule } from '../events/events.module'; + +@Module({ + imports: [PrismaModule, EventsModule], + controllers: [ReservationsController], + providers: [ReservationsService], + exports: [ReservationsService], +}) +export class ReservationsModule {} diff --git a/apps/api/src/reservations/reservations.service.ts b/apps/api/src/reservations/reservations.service.ts new file mode 100644 index 0000000..50a3828 --- /dev/null +++ b/apps/api/src/reservations/reservations.service.ts @@ -0,0 +1,69 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import { EventsGateway } from '../events/events.gateway'; +import { CreateReservationDto } from './dto/create-reservation.dto'; + +@Injectable() +export class ReservationsService { + constructor( + private prisma: PrismaService, + private eventsGateway: EventsGateway, + ) {} + + async create(userId: string, createReservationDto: CreateReservationDto) { + const { spaceId, startTime, endTime } = createReservationDto; + + const space = await this.prisma.space.findUnique({ + where: { id: spaceId }, + }); + + if (!space) { + throw new NotFoundException(`Space with ID ${spaceId} not found`); + } + + const reservation = await this.prisma.reservation.create({ + data: { + startTime: new Date(startTime), + endTime: new Date(endTime), + userId, + spaceId, + }, + }); + + // Notify clients on the floor + this.eventsGateway.emitSpaceStatusChanged(space.floorId, spaceId, true); + + return reservation; + } + + async cancel(reservationId: string) { + const reservation = await this.prisma.reservation.findUnique({ + where: { id: reservationId }, + include: { space: true }, + }); + + if (!reservation) { + throw new NotFoundException(`Reservation with ID ${reservationId} not found`); + } + + await this.prisma.reservation.delete({ + where: { id: reservationId }, + }); + + // Notify clients on the floor + this.eventsGateway.emitSpaceStatusChanged( + reservation.space.floorId, + reservation.spaceId, + false, + ); + + return { message: 'Reservation cancelled' }; + } + + async findAllByUserId(userId: string) { + return this.prisma.reservation.findMany({ + where: { userId }, + include: { space: true }, + }); + } +} diff --git a/apps/api/src/spaces/spaces.service.ts b/apps/api/src/spaces/spaces.service.ts index e6a645e..7670c2d 100644 --- a/apps/api/src/spaces/spaces.service.ts +++ b/apps/api/src/spaces/spaces.service.ts @@ -1,15 +1,30 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class SpacesService { + private readonly logger = new Logger(SpacesService.name); + constructor(private prisma: PrismaService) {} async getFloorLayout(floorId: string) { return this.prisma.floor.findUnique({ where: { id: floorId }, include: { - spaces: true, + spaces: { + include: { + reservations: { + where: { + endTime: { + gte: new Date(), + }, + startTime: { + lte: new Date(), + }, + }, + }, + }, + }, building: { include: { organization: true, diff --git a/apps/web/package.json b/apps/web/package.json index 689427a..df15b74 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,6 +26,7 @@ }, "type": "module", "dependencies": { - "import-meta-resolve": "^4.2.0" + "import-meta-resolve": "^4.2.0", + "socket.io-client": "^4.8.3" } } diff --git a/apps/web/src/lib/api/client.ts b/apps/web/src/lib/api/client.ts index 7282e79..36bd578 100644 --- a/apps/web/src/lib/api/client.ts +++ b/apps/web/src/lib/api/client.ts @@ -2,9 +2,10 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000 export async function apiFetch( path: string, - options: RequestInit = {}, + options: RequestInit & { fetch?: typeof fetch } = {}, ): Promise { - const res = await fetch(`${API_BASE_URL}${path}`, { + const fetchFn = options.fetch ?? fetch; + const res = await fetchFn(`${API_BASE_URL}${path}`, { headers: { 'Content-Type': 'application/json', ...(options.headers ?? {}), diff --git a/apps/web/src/lib/components/SpaceCard.svelte b/apps/web/src/lib/components/SpaceCard.svelte new file mode 100644 index 0000000..ef4ce52 --- /dev/null +++ b/apps/web/src/lib/components/SpaceCard.svelte @@ -0,0 +1,55 @@ + + + + + + {name} + {type} + + + + {isOccupied ? 'Occupied' : 'Available'} + + + + diff --git a/apps/web/src/routes/dashboard/floor/[id]/+page.svelte b/apps/web/src/routes/dashboard/floor/[id]/+page.svelte index 09d2cca..93de119 100644 --- a/apps/web/src/routes/dashboard/floor/[id]/+page.svelte +++ b/apps/web/src/routes/dashboard/floor/[id]/+page.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import { token } from "$lib/stores/auth"; import { goto } from "$app/navigation"; + import SpaceCard from '$lib/components/SpaceCard.svelte'; let floor: any = null; let loading = true; @@ -10,18 +11,22 @@ onMount(async () => { const floorId = $page.params.id; if (!$token) { - goto("/login"); + goto(`/login?redirect=/dashboard/floor/${floorId}`); return; } try { - const res = await fetch(`http://localhost:3000/spaces/floor/${floorId}`, { + const apiBase = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000'; + const res = await fetch(`${apiBase}/spaces/floor/${floorId}`, { headers: { - Authorization: `Bearer ${$token}`, + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${$token}`, }, }); if (res.ok) { floor = await res.json(); + } else if (res.status === 401) { + goto("/login"); } } catch (e) { console.error(e); @@ -42,10 +47,13 @@ {#each floor.spaces as space} - - {space.name} - {space.type} - + 0} + /> {/each} {:else} diff --git a/apps/web/src/routes/dashboard/floor/[id]/+page.ts b/apps/web/src/routes/dashboard/floor/[id]/+page.ts new file mode 100644 index 0000000..b129451 --- /dev/null +++ b/apps/web/src/routes/dashboard/floor/[id]/+page.ts @@ -0,0 +1,8 @@ +import { error } from '@sveltejs/kit'; +import { apiFetch } from '$lib/api/client'; + +export const load = async () => { + return {}; +}; + +export const ssr = false; diff --git a/package-lock.json b/package-lock.json index fae8e6d..6456a92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,8 @@ "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^10.4.22", + "@nestjs/platform-socket.io": "^10.4.22", + "@nestjs/websockets": "^10.4.22", "@prisma/client": "^6.4.0", "@types/bcrypt": "^6.0.0", "@types/passport-jwt": "^4.0.1", @@ -30,7 +32,8 @@ "passport-jwt": "^4.0.1", "prisma": "^6.4.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.2" + "rxjs": "^7.8.2", + "socket.io": "^4.8.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -61,7 +64,8 @@ "apps/web": { "version": "0.0.1", "dependencies": { - "import-meta-resolve": "^4.2.0" + "import-meta-resolve": "^4.2.0", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.3.1", @@ -2498,6 +2502,57 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-socket.io": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.22.tgz", + "integrity": "sha512-xxGw3R0Ihr51/Omq23z3//bKmCXyVKaikxbH0/pkwqMsQrxkUv9NabNUZ22b4Jnlwwi02X+zlwo8GRa9u8oV9g==", + "dependencies": { + "socket.io": "4.8.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-socket.io/node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, "node_modules/@nestjs/schematics": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.2.3.tgz", @@ -2547,6 +2602,28 @@ } } }, + "node_modules/@nestjs/websockets": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.22.tgz", + "integrity": "sha512-OLd4i0Faq7vgdtB5vVUrJ54hWEtcXy9poJ6n7kbbh/5ms+KffUl+wwGsbe7uSXLrkoyI8xXU6fZPkFArI+XiRg==", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -3069,6 +3146,11 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3309,6 +3391,14 @@ "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", "dev": true }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -3527,6 +3617,14 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==" }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -4642,6 +4740,14 @@ } ] }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.11", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", @@ -5757,6 +5863,54 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/enhanced-resolve": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", @@ -8860,7 +9014,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "engines": { "node": ">= 6" } @@ -10198,6 +10351,58 @@ "node": ">=8" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sorcery": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", @@ -11803,6 +12008,34 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
{type}