Add real-time space status updates and reservation management

- Integrate WebSocket functionality using `socket.io` for floor-specific updates.
- Implement `ReservationsModule` with CRUD for managing reservations (API).
- Create `LoggerMiddleware` for consistent HTTP request logging.
- Update `SpacesService` to include real-time reservation status checks.
- Add `SpaceCard.svelte` component to reflect live space status (Web).
- Enable environmental base API URL configuration for flexible deployments.
main
Pau Costa Ferrer 2026-03-28 00:50:51 +01:00
parent 8dc99bc1a4
commit 3b09daeb04
19 changed files with 611 additions and 45 deletions

View File

@ -4,21 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Remove compiled API files from version control&#10;&#10;- Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder.">
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<list default="true" id="6c8b3c54-eabb-40e5-967f-c7b594c750bc" name="Changes" comment="Implement authentication, organization management, and floor layout features&#10;&#10;- Add API authentication with JWT strategy using NestJS.&#10;- Configure Prisma schema for organizations, users, buildings, floors, and spaces.&#10;- Seed initial database with test data and relationships.&#10;- Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display.&#10;- Remove deprecated `AppService` from API.">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/dist/app.service.d.ts" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/prisma/schema.prisma" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/app.module.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/app.module.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/main.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/main.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/prisma/prisma.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/prisma/prisma.service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.controller.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/api/src/spaces/spaces.service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+layout.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/login/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/lib/api/client.ts" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/lib/api/client.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" beforeDir="false" afterPath="$PROJECT_DIR$/apps/web/src/routes/dashboard/floor/[id]/+page.svelte" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
@ -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"
]
}
}]]></component>
<component name="RunManager" selected="npm.web &gt; dev">
@ -130,7 +129,7 @@
<option name="presentableId" value="Default" />
<updated>1774643312599</updated>
<workItem from="1774643313611" duration="74000" />
<workItem from="1774643422240" duration="4582000" />
<workItem from="1774643422240" duration="5278000" />
</task>
<task id="LOCAL-00001" summary="Initialize Hot Desking app with web and API services &#10;&#10;- Set up SvelteKit for the web interface with Tailwind CSS. &#10;- Build the API using NestJS with Prisma ORM for database interaction. &#10;- Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. &#10;- Create shared data models for consistent typing across services. &#10;- Establish basic routing and layouts for web and API services.">
<option name="closed" value="true" />
@ -148,7 +147,15 @@
<option name="project" value="LOCAL" />
<updated>1774653909300</updated>
</task>
<option name="localTasksCounter" value="3" />
<task id="LOCAL-00003" summary="Implement authentication, organization management, and floor layout features&#10;&#10;- Add API authentication with JWT strategy using NestJS.&#10;- Configure Prisma schema for organizations, users, buildings, floors, and spaces.&#10;- Seed initial database with test data and relationships.&#10;- Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display.&#10;- Remove deprecated `AppService` from API.">
<option name="closed" value="true" />
<created>1774654565964</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1774654565964</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -157,6 +164,7 @@
<component name="VcsManagerConfiguration">
<MESSAGE value="Initialize Hot Desking app with web and API services &#10;&#10;- Set up SvelteKit for the web interface with Tailwind CSS. &#10;- Build the API using NestJS with Prisma ORM for database interaction. &#10;- Add environment variable management and docker-compose for PostgreSQL and PgAdmin instances. &#10;- Create shared data models for consistent typing across services. &#10;- Establish basic routing and layouts for web and API services." />
<MESSAGE value="Remove compiled API files from version control&#10;&#10;- Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder." />
<option name="LAST_COMMIT_MESSAGE" value="Remove compiled API files from version control&#10;&#10;- Deleted compiled JavaScript files, type definitions, and source maps from the `dist` folder." />
<MESSAGE value="Implement authentication, organization management, and floor layout features&#10;&#10;- Add API authentication with JWT strategy using NestJS.&#10;- Configure Prisma schema for organizations, users, buildings, floors, and spaces.&#10;- Seed initial database with test data and relationships.&#10;- Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display.&#10;- Remove deprecated `AppService` from API." />
<option name="LAST_COMMIT_MESSAGE" value="Implement authentication, organization management, and floor layout features&#10;&#10;- Add API authentication with JWT strategy using NestJS.&#10;- Configure Prisma schema for organizations, users, buildings, floors, and spaces.&#10;- Seed initial database with test data and relationships.&#10;- Build SvelteKit web pages for dashboard and floor layout with secured routing and organization hierarchy display.&#10;- Remove deprecated `AppService` from API." />
</component>
</project>

View File

@ -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",

View File

@ -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
}

View File

@ -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('*');
}
}

View File

@ -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';

View File

@ -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();
}
}

View File

@ -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,
});
}
}

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';
@Module({
providers: [EventsGateway],
exports: [EventsGateway],
})
export class EventsModule {}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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 },
});
}
}

View File

@ -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,

View File

@ -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"
}
}

View File

@ -2,9 +2,10 @@ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000
export async function apiFetch<T>(
path: string,
options: RequestInit = {},
options: RequestInit & { fetch?: typeof fetch } = {},
): Promise<T> {
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 ?? {}),

View File

@ -0,0 +1,55 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { io, Socket } from 'socket.io-client';
export let spaceId: string;
export let name: string;
export let type: string;
export let floorId: string;
export let initialStatus: boolean = false; // true if occupied
let isOccupied = initialStatus;
let socket: Socket;
onMount(() => {
// Connect to WebSocket using the environment variable for the backend URL.
const socketUrl = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000';
socket = io(socketUrl);
socket.on('connect', () => {
console.log('Connected to WebSocket');
socket.emit('join_floor', floorId);
});
socket.on('space_status_changed', (data: { spaceId: string, isOccupied: boolean }) => {
if (data.spaceId === spaceId) {
isOccupied = data.isOccupied;
}
});
socket.on('disconnect', () => {
console.log('Disconnected from WebSocket');
});
});
onDestroy(() => {
if (socket) {
socket.emit('leave_floor', floorId);
socket.disconnect();
}
});
</script>
<div class="p-4 border rounded shadow-md {isOccupied ? 'bg-red-100 border-red-500' : 'bg-green-100 border-green-500'}">
<div class="flex justify-between items-center">
<div>
<h3 class="font-bold text-lg">{name}</h3>
<p class="text-sm text-gray-600">{type}</p>
</div>
<div class="flex flex-col items-end">
<span class="px-2 py-1 rounded text-xs font-semibold {isOccupied ? 'bg-red-500 text-white' : 'bg-green-500 text-white'}">
{isOccupied ? 'Occupied' : 'Available'}
</span>
</div>
</div>
</div>

View File

@ -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 @@
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{#each floor.spaces as space}
<div class="border p-4 rounded bg-gray-50 flex flex-col items-center justify-center text-center">
<span class="font-semibold">{space.name}</span>
<span class="text-xs text-gray-500 uppercase">{space.type}</span>
</div>
<SpaceCard
spaceId={space.id}
name={space.name}
type={space.type}
floorId={floor.id}
initialStatus={space.reservations && space.reservations.length > 0}
/>
{/each}
</div>
{:else}

View File

@ -0,0 +1,8 @@
import { error } from '@sveltejs/kit';
import { apiFetch } from '$lib/api/client';
export const load = async () => {
return {};
};
export const ssr = false;

239
package-lock.json generated
View File

@ -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",