From e842a19660d0378434867c761d1bf21d123bd5a9 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 20:54:59 +0300 Subject: [PATCH 01/47] feat(infra): setup multi-stage dockerfiles and compose orchestration --- .dockerignore | 41 ++++++++++++++++++++++++++++++++ .env.example | 20 ++++++++++++++-- Dockerfile.dev | 14 +++++++++++ Dockerfile.prod | 35 +++++++++++++++++++++++++++ infra/compose.dev.yaml | 54 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.dev create mode 100644 Dockerfile.prod create mode 100644 infra/compose.dev.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dbafb72 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules +npm-debug.log +yarn-error.log +pnpm-debug.log + +# Build output +dist +artifacts +out + +# Environment variables +.env +.env.production +.env.local +!.env.example + +# Docker / Infrastructure +docker-compose.yml +docker-compose.*.yml +Dockerfile +Dockerfile.* +.dockerignore + +# Git +.git +.gitignore +.gitattributes + +# Editor / OS +.vscode +.idea +.DS_Store +*.swp +*.log + +# Tests and Coverage +coverage +test-results +*.spec.ts +*.e2e-spec.ts diff --git a/.env.example b/.env.example index 826b5f6..492df4c 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,19 @@ -PORT=3005 -DATABASE_URL="postgresql://postgres:root@localhost:5432/task-tracker" +DB_USERNAME=admin +DB_PASSWORD=p@ssword123 +DB_DATABASE=task_tracker + +DB_PORT=6000 + +# Строка подключения для NestJS (через сервис 'database') +# ВАЖНО: Внутри сети Docker используем порт 5432 и имя сервиса 'database' +# DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@database:5432/${DB_DATABASE} + +# Если во вне работаете с образами базы данных +DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:6000/${DB_DATABASE} + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_EXTERNAL_PORT=6380 + +PORT=3000 NODE_ENV=development diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..55b2adf --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,14 @@ +FROM node:23-alpine + +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ +COPY tsconfig* ./ + +RUN pnpm install --no-frozen-lockfile + +COPY . . + +CMD ["pnpm", "run", "start:dev"] \ No newline at end of file diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..16325c9 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,35 @@ +FROM node:20-alpine AS base + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /app + +COPY package.json pnpm-lock.yaml ./ + +FROM base AS build + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --frozen-lockfile + +COPY . . + +RUN pnpm run build + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm prune --prod + +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 + +COPY --from=build /app/dist ./dist +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/package.json ./ + +EXPOSE 3000 + +CMD ["node", "dist/main"] \ No newline at end of file diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml new file mode 100644 index 0000000..7c54f3a --- /dev/null +++ b/infra/compose.dev.yaml @@ -0,0 +1,54 @@ +version: "3.9" + +name: task-tracker + +services: + api: + hostname: api + container_name: api + build: + context: ../ + dockerfile: Dockerfile.dev + restart: always + env_file: + - ../.env + ports: + - "3000:3000" + depends_on: + database: + condition: service_healthy + redis: + condition: service_started + networks: + - backend + + database: + hostname: database + container_name: database + image: postgres:16-alpine + restart: always + env_file: + - ../.env + environment: + POSTGRES_USER: ${DB_USERNAME:-admin} + POSTGRES_PASSWORD: ${DB_PASSWORD:-admin} + POSTGRES_DB: ${DB_DATABASE:-tracker} + ports: + - "6000:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}"] + interval: 5s + timeout: 5s + retries: 5 + profiles: ["infra"] + +volumes: + postgres_data: + redis_data: + +networks: + backend: From 8019995b49cac375d6fd840179669d2b1dd5689c Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 21:14:07 +0300 Subject: [PATCH 02/47] chore: resolve merge conflicts --- .env.example | 19 +++++++++++-------- libs/config/src/config.schema.ts | 7 +++++++ .../interfaces/database-module.interface.ts | 7 +++++-- src/app.module.ts | 18 +++++++++++++++++- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 492df4c..f981c6e 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,22 @@ +# --- APP --- +PORT=3000 +NODE_ENV=development + +# --- POSTGRES --- DB_USERNAME=admin DB_PASSWORD=p@ssword123 DB_DATABASE=task_tracker - DB_PORT=6000 +DB_SCHEMA=base -# Строка подключения для NestJS (через сервис 'database') -# ВАЖНО: Внутри сети Docker используем порт 5432 и имя сервиса 'database' +# ВАЖНО: +# Для работы ВНУТРИ Docker: используй хост 'database' и порт 5432 # DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@database:5432/${DB_DATABASE} -# Если во вне работаете с образами базы данных -DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:6000/${DB_DATABASE} +# Для работы ЛОКАЛЬНО (без докера): используй 'localhost' и порт 6000 +DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} +# --- REDIS --- REDIS_HOST=redis REDIS_PORT=6379 REDIS_EXTERNAL_PORT=6380 - -PORT=3000 -NODE_ENV=development diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 6042da3..172e6b3 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -3,6 +3,13 @@ import { z } from 'zod/v4'; export const ConfigSchema = z.object({ PORT: z.coerce.number().default(3000), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + DB_USERNAME: z.string({ error: 'DB_USERNAME is missing' }), + DB_PASSWORD: z.string({ error: 'DB_PASSWORD is missing' }), + DB_DATABASE: z.string({ error: 'DB_DATABASE is missing' }), + DB_SCHEMA: z.string({ error: 'DB_SCHEMA is missing' }), + DATABASE_URL: z.string().url('DATABASE_URL must be a valid connection string'), + REDIS_HOST: z.string().default('redis'), + REDIS_PORT: z.coerce.number().default(6379), }); export type Config = z.infer; diff --git a/libs/database/src/interfaces/database-module.interface.ts b/libs/database/src/interfaces/database-module.interface.ts index 55e114e..7926881 100644 --- a/libs/database/src/interfaces/database-module.interface.ts +++ b/libs/database/src/interfaces/database-module.interface.ts @@ -12,11 +12,14 @@ export interface DatabaseModuleOptionsFactory { createDatabaseOptions(): Promise | DatabaseModuleOptions; } -export interface DatabaseModuleAsyncOptions extends Pick { +export interface DatabaseModuleAsyncOptions extends Pick< + ModuleMetadata, + 'imports' +> { useExisting?: Type; useClass?: Type; useFactory?: ( - ...args: unknown[] + ...args: TArgs ) => Promise> | Omit; inject?: FactoryProvider['inject']; global?: boolean; diff --git a/src/app.module.ts b/src/app.module.ts index d4fff82..74ca97c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,9 +2,25 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ConfigModule } from '@libs/config'; +import { DatabaseModule } from '@libs/database'; +import { ConfigService } from '@nestjs/config'; +import * as schema from './shared/entities'; @Module({ - imports: [ConfigModule], + imports: [ + ConfigModule, + DatabaseModule.registerAsync({ + global: true, + inject: [ConfigService], + useFactory: (cfg: ConfigService) => { + return { + schema, + schemaName: cfg.getOrThrow('DB_SCHEMA'), + logging: true, + }; + }, + }), + ], controllers: [AppController], providers: [AppService], }) From 90889cb1c0357cc963518d761ffdcfd813571d0b Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 21:25:46 +0300 Subject: [PATCH 03/47] chore: add api boilerplate packages --- package.json | 8 +- pnpm-lock.yaml | 792 ++++++++++++++++++++++++++++++++++++------- test/app.e2e-spec.ts | 4 +- 3 files changed, 683 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index f2c89ff..52f8530 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,17 @@ "db:studio": "drizzle-kit studio" }, "dependencies": { + "@fastify/compress": "^8.3.1", + "@fastify/cookie": "^11.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^4.0.4", "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-fastify": "^11.1.18", + "@nestjs/swagger": "^11.2.7", + "@nestjs/throttler": "^6.5.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", + "fastify": "^5.8.4", "pg": "^8.20.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -38,7 +43,6 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", "@types/node": "^20.3.1", "@types/pg": "^8.20.0", "@types/supertest": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21fa10b..a7b7109 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@fastify/compress': + specifier: ^8.3.1 + version: 8.3.1 + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.0.2 '@nestjs/common': specifier: ^10.0.0 version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -17,15 +23,24 @@ importers: '@nestjs/core': specifier: ^10.0.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + '@nestjs/platform-fastify': + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + '@nestjs/swagger': + specifier: ^11.2.7 + version: 11.2.7(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2) drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0) drizzle-zod: specifier: ^0.8.3 version: 0.8.3(drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6) + fastify: + specifier: ^5.8.4 + version: 5.8.4 pg: specifier: ^8.20.0 version: 8.20.0 @@ -48,9 +63,6 @@ importers: '@nestjs/testing': specifier: ^10.0.0 version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22) - '@types/express': - specifier: ^4.17.17 - version: 4.17.25 '@types/node': specifier: ^20.3.1 version: 20.19.39 @@ -650,6 +662,39 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/compress@8.3.1': + resolution: {integrity: sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==} + + '@fastify/cookie@11.0.2': + resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + + '@fastify/cors@11.2.0': + resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/formbody@8.0.2': + resolution: {integrity: sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -697,6 +742,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -752,17 +800,60 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/platform-express@10.4.22': resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 + '@nestjs/platform-fastify@11.1.18': + resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/view': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + '@fastify/view': + optional: true + '@nestjs/schematics@10.2.3': resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==} peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@11.2.7': + resolution: {integrity: sha512-+e1KWSyZMAQeyZ8nbQSvm3fhzqdxxBNQENvpjO2dVyD7KJmLTTQyXpRb1nM5O04oFdDTUtG3SHMl4+e+zgCK2A==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@10.4.22': resolution: {integrity: sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==} peerDependencies: @@ -776,6 +867,13 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -803,6 +901,9 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -918,6 +1019,9 @@ packages: rollup: optional: true + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1036,15 +1140,9 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -1060,48 +1158,21 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} - '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} @@ -1258,6 +1329,13 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1284,6 +1362,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -1367,6 +1453,13 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1410,6 +1503,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1552,6 +1648,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cookiejar@2.1.4: resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} @@ -1617,6 +1717,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -1758,6 +1862,12 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1780,6 +1890,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} @@ -1920,6 +2033,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} @@ -1939,6 +2056,9 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1952,15 +2072,27 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.8.4: + resolution: {integrity: sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==} + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1996,6 +2128,10 @@ packages: resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} engines: {node: '>= 0.8'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2172,6 +2308,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -2211,6 +2351,9 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2253,6 +2396,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2283,6 +2429,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -2558,6 +2707,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2630,6 +2783,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2637,6 +2793,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -2686,6 +2845,16 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2723,10 +2892,29 @@ packages: engines: {node: '>=14'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pumpify@2.0.1: + resolution: {integrity: sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2742,6 +2930,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2750,14 +2941,25 @@ packages: resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} engines: {node: '>= 0.8'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -2784,6 +2986,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2818,9 +3024,20 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -2832,6 +3049,9 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2845,6 +3065,9 @@ packages: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -2898,6 +3121,9 @@ packages: resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} engines: {node: '>=20'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2931,6 +3157,9 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -2955,6 +3184,9 @@ packages: resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} engines: {node: '>=20'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2996,6 +3228,9 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + swagger-ui-dist@5.32.2: + resolution: {integrity: sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -3032,6 +3267,13 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -3058,6 +3300,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} @@ -3708,6 +3954,57 @@ snapshots: '@eslint/js@8.57.1': {} + '@fastify/accept-negotiator@2.0.1': {} + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + + '@fastify/compress@8.3.1': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + fastify-plugin: 5.1.0 + mime-db: 1.52.0 + minipass: 7.1.3 + peek-stream: 1.1.3 + pump: 3.0.4 + pumpify: 2.0.1 + readable-stream: 4.7.0 + + '@fastify/cookie@11.0.2': + dependencies: + cookie: 1.1.1 + fastify-plugin: 5.1.0 + + '@fastify/cors@11.2.0': + dependencies: + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/formbody@8.0.2': + dependencies: + fast-querystring: 1.1.2 + fastify-plugin: 5.1.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -3764,6 +4061,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -3834,6 +4133,11 @@ snapshots: transitivePeerDependencies: - encoding + '@nestjs/mapped-types@2.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3845,6 +4149,22 @@ snapshots: tslib: 2.8.1 transitivePeerDependencies: - supports-color + optional: true + + '@nestjs/platform-fastify@11.1.18(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': + dependencies: + '@fastify/cors': 11.2.0 + '@fastify/formbody': 8.0.2 + '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-querystring: 1.1.2 + fastify: 5.8.4 + fastify-plugin: 5.1.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + path-to-regexp: 8.4.2 + reusify: 1.1.0 + tslib: 2.8.1 '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.7.2)': dependencies: @@ -3868,6 +4188,18 @@ snapshots: transitivePeerDependencies: - chokidar + '@nestjs/swagger@11.2.7(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.2 + '@nestjs/testing@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3876,6 +4208,12 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + '@nestjs/throttler@6.5.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -3904,6 +4242,8 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -3966,6 +4306,8 @@ snapshots: estree-walker: 2.0.2 picomatch: 4.0.4 + '@scarf/scarf@1.4.0': {} + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -4051,20 +4393,11 @@ snapshots: tslib: 2.8.1 optional: true - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 20.19.39 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 20.19.39 - '@types/cookiejar@2.1.5': {} '@types/deep-eql@4.0.2': {} @@ -4081,28 +4414,10 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 20.19.39 - '@types/qs': 6.15.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.15.0 - '@types/serve-static': 1.15.10 - - '@types/http-errors@2.0.5': {} - '@types/json-schema@7.0.15': {} '@types/methods@1.1.4': {} - '@types/mime@1.3.5': {} - '@types/node@20.19.39': dependencies: undici-types: 6.21.0 @@ -4113,27 +4428,8 @@ snapshots: pg-protocol: 1.13.0 pg-types: 2.2.0 - '@types/qs@6.15.0': {} - - '@types/range-parser@1.2.7': {} - '@types/semver@7.7.1': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 20.19.39 - - '@types/send@1.2.1': - dependencies: - '@types/node': 20.19.39 - - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 20.19.39 - '@types/send': 0.17.6 - '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 @@ -4369,10 +4665,17 @@ snapshots: '@xtuc/long@4.2.2': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 + optional: true acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -4392,6 +4695,10 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv-keywords@3.5.2(ajv@6.14.0): dependencies: ajv: 6.14.0 @@ -4447,13 +4754,15 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 - append-field@1.0.0: {} + append-field@1.0.0: + optional: true arg@4.1.3: {} argparse@2.0.1: {} - array-flatten@1.1.1: {} + array-flatten@1.1.1: + optional: true array-timsort@1.0.3: {} @@ -4471,6 +4780,13 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.1 + balanced-match@1.0.2: {} base64-js@1.5.1: {} @@ -4501,6 +4817,7 @@ snapshots: unpipe: 1.0.0 transitivePeerDependencies: - supports-color + optional: true brace-expansion@1.1.13: dependencies: @@ -4530,11 +4847,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 + optional: true - bytes@3.1.2: {} + bytes@3.1.2: + optional: true call-bind-apply-helpers@1.0.2: dependencies: @@ -4645,20 +4969,27 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 typedarray: 0.0.6 + optional: true consola@2.15.3: {} content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 + optional: true - content-type@1.0.5: {} + content-type@1.0.5: + optional: true convert-source-map@2.0.0: {} - cookie-signature@1.0.7: {} + cookie-signature@1.0.7: + optional: true + + cookie@0.7.2: + optional: true - cookie@0.7.2: {} + cookie@1.1.1: {} cookiejar@2.1.4: {} @@ -4668,6 +4999,7 @@ snapshots: dependencies: object-assign: 4.1.1 vary: 1.1.2 + optional: true cosmiconfig@8.3.6(typescript@5.7.2): dependencies: @@ -4689,6 +5021,7 @@ snapshots: debug@2.6.9: dependencies: ms: 2.0.0 + optional: true debug@4.4.3: dependencies: @@ -4710,9 +5043,13 @@ snapshots: delayed-stream@1.0.0: {} - depd@2.0.0: {} + depd@2.0.0: + optional: true + + dequal@2.0.3: {} - destroy@1.2.0: {} + destroy@1.2.0: + optional: true detect-libc@2.1.2: {} @@ -4762,9 +5099,24 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexify@3.7.1: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + eastasianwidth@0.2.0: {} - ee-first@1.1.1: {} + ee-first@1.1.1: + optional: true electron-to-chromium@1.5.334: {} @@ -4774,7 +5126,12 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} + encodeurl@2.0.0: + optional: true + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 enhanced-resolve@5.20.1: dependencies: @@ -4891,7 +5248,8 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: {} + escape-html@1.0.3: + optional: true escape-string-regexp@1.0.5: {} @@ -4994,7 +5352,10 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} + etag@1.8.1: + optional: true + + event-target-shim@5.0.1: {} eventemitter3@5.0.4: {} @@ -5037,6 +5398,7 @@ snapshots: vary: 1.1.2 transitivePeerDependencies: - supports-color + optional: true external-editor@3.1.0: dependencies: @@ -5044,6 +5406,8 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -5058,12 +5422,45 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-safe-stringify@2.1.1: {} fast-uri@3.1.0: {} + fastify-plugin@5.1.0: {} + + fastify@5.8.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.4 + toad-cache: 3.7.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -5106,6 +5503,13 @@ snapshots: unpipe: 1.0.0 transitivePeerDependencies: - supports-color + optional: true + + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 find-up@5.0.0: dependencies: @@ -5157,9 +5561,11 @@ snapshots: once: 1.4.0 qs: 6.15.1 - forwarded@0.2.0: {} + forwarded@0.2.0: + optional: true - fresh@0.5.2: {} + fresh@0.5.2: + optional: true fs-extra@10.1.0: dependencies: @@ -5274,6 +5680,7 @@ snapshots: setprototypeof: 1.2.0 statuses: 2.0.2 toidentifier: 1.0.1 + optional: true iconv-lite@0.4.24: dependencies: @@ -5333,7 +5740,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - ipaddr.js@1.9.1: {} + ipaddr.js@1.9.1: + optional: true + + ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -5361,6 +5771,8 @@ snapshots: is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5402,6 +5814,10 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -5429,6 +5845,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.32.0: optional: true @@ -5547,13 +5969,15 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@0.3.0: {} + media-typer@0.3.0: + optional: true memfs@3.5.3: dependencies: fs-monkey: 1.1.0 - merge-descriptors@1.0.3: {} + merge-descriptors@1.0.3: + optional: true merge-stream@2.0.0: {} @@ -5572,7 +5996,8 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} + mime@1.6.0: + optional: true mime@2.6.0: {} @@ -5599,8 +6024,10 @@ snapshots: mkdirp@0.5.6: dependencies: minimist: 1.2.8 + optional: true - ms@2.0.0: {} + ms@2.0.0: + optional: true ms@2.1.3: {} @@ -5613,6 +6040,7 @@ snapshots: object-assign: 4.1.1 type-is: 1.6.18 xtend: 4.0.2 + optional: true mute-stream@0.0.8: {} @@ -5622,7 +6050,8 @@ snapshots: natural-compare@1.4.0: {} - negotiator@0.6.3: {} + negotiator@0.6.3: + optional: true neo-async@2.6.2: {} @@ -5640,15 +6069,19 @@ snapshots: normalize-path@3.0.0: {} - object-assign@4.1.1: {} + object-assign@4.1.1: + optional: true object-inspect@1.13.4: {} obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 + optional: true once@1.4.0: dependencies: @@ -5706,7 +6139,8 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parseurl@1.3.3: {} + parseurl@1.3.3: + optional: true path-exists@4.0.0: {} @@ -5719,14 +6153,23 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 - path-to-regexp@0.1.13: {} + path-to-regexp@0.1.13: + optional: true path-to-regexp@3.3.0: {} + path-to-regexp@8.4.2: {} + path-type@4.0.0: {} pathe@2.0.3: {} + peek-stream@1.1.3: + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + pg-cloudflare@1.3.0: optional: true @@ -5770,6 +6213,26 @@ snapshots: picomatch@4.0.4: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pluralize@8.0.0: {} postcss@8.5.9: @@ -5796,16 +6259,37 @@ snapshots: prettier@3.8.2: {} + process-nextick-args@2.0.1: {} + + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + + process@0.11.10: {} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pumpify@2.0.1: + dependencies: + duplexify: 4.1.3 + inherits: 2.0.4 + pump: 3.0.4 punycode@2.3.1: {} qs@6.14.2: dependencies: side-channel: 1.1.0 + optional: true qs@6.15.1: dependencies: @@ -5813,7 +6297,10 @@ snapshots: queue-microtask@1.2.3: {} - range-parser@1.2.1: {} + quick-format-unescaped@4.0.4: {} + + range-parser@1.2.1: + optional: true raw-body@2.5.3: dependencies: @@ -5821,6 +6308,17 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.4.24 unpipe: 1.0.0 + optional: true + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 readable-stream@3.6.2: dependencies: @@ -5828,10 +6326,20 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 + real-require@0.2.0: {} + reflect-metadata@0.2.2: {} repeat-string@1.6.1: {} @@ -5852,6 +6360,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + ret@0.5.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -5897,8 +6407,16 @@ snapshots: dependencies: tslib: 2.8.1 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} schema-utils@3.3.0: @@ -5914,6 +6432,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + secure-json-parse@4.1.0: {} + semver@7.7.4: {} send@0.19.2: @@ -5933,6 +6453,7 @@ snapshots: statuses: 2.0.2 transitivePeerDependencies: - supports-color + optional: true serve-static@1.16.3: dependencies: @@ -5942,6 +6463,9 @@ snapshots: send: 0.19.2 transitivePeerDependencies: - supports-color + optional: true + + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -5952,7 +6476,8 @@ snapshots: gopd: 1.2.0 has-property-descriptors: 1.0.2 - setprototypeof@1.2.0: {} + setprototypeof@1.2.0: + optional: true shebang-command@2.0.0: dependencies: @@ -6006,6 +6531,10 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -6023,11 +6552,15 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.2: {} + statuses@2.0.2: + optional: true std-env@4.0.0: {} - streamsearch@1.1.0: {} + stream-shift@1.0.3: {} + + streamsearch@1.1.0: + optional: true string-argv@0.3.2: {} @@ -6054,6 +6587,10 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6104,6 +6641,10 @@ snapshots: dependencies: has-flag: 4.0.0 + swagger-ui-dist@5.32.2: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} synckit@0.11.12: @@ -6132,6 +6673,15 @@ snapshots: text-table@0.2.0: {} + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + + through2@2.0.5: + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + through@2.3.8: {} tinybench@2.9.0: {} @@ -6153,7 +6703,10 @@ snapshots: dependencies: is-number: 7.0.0 - toidentifier@1.0.1: {} + toad-cache@3.7.0: {} + + toidentifier@1.0.1: + optional: true token-types@6.1.2: dependencies: @@ -6233,8 +6786,10 @@ snapshots: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 + optional: true - typedarray@0.0.6: {} + typedarray@0.0.6: + optional: true typescript@5.7.2: {} @@ -6250,7 +6805,8 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: {} + unpipe@1.0.0: + optional: true unplugin-swc@1.5.9(@swc/core@1.15.24): dependencies: @@ -6280,11 +6836,13 @@ snapshots: util-deprecate@1.0.2: {} - utils-merge@1.0.1: {} + utils-merge@1.0.1: + optional: true v8-compile-cache-lib@3.0.1: {} - vary@1.1.2: {} + vary@1.1.2: + optional: true vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4d26f6b..95c5212 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; +import { agent } from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { @@ -16,6 +16,6 @@ describe('AppController (e2e)', () => { }); it('/ (GET)', () => { - return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); + return agent(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); }); }); From a231cdb6ba93bd677e4de1e240cdd4981a11fb7a Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 21:45:51 +0300 Subject: [PATCH 04/47] feat(bootstrap): implement unified bootstrap service and fix fastify issues --- .env.example | 1 + libs/bootstrap/src/bootstrap.ts | 81 + libs/bootstrap/src/configs/swagger.ts | 7 + libs/bootstrap/src/configs/throttler.ts | 9 + libs/bootstrap/src/index.ts | 1 + libs/bootstrap/src/interfaces/index.ts | 1 + .../src/interfaces/options.interface.ts | 35 + libs/bootstrap/src/setups/cors.ts | 27 + libs/bootstrap/src/setups/index.ts | 3 + libs/bootstrap/src/setups/swagger.ts | 38 + libs/bootstrap/src/setups/throttler.ts | 19 + libs/bootstrap/tsconfig.lib.json | 9 + libs/config/src/config.schema.ts | 19 + nest-cli.json | 11 +- package.json | 137 +- pnpm-lock.yaml | 1691 ++++++----------- src/main.ts | 28 +- tsconfig.json | 4 +- 18 files changed, 971 insertions(+), 1150 deletions(-) create mode 100644 libs/bootstrap/src/bootstrap.ts create mode 100644 libs/bootstrap/src/configs/swagger.ts create mode 100644 libs/bootstrap/src/configs/throttler.ts create mode 100644 libs/bootstrap/src/index.ts create mode 100644 libs/bootstrap/src/interfaces/index.ts create mode 100644 libs/bootstrap/src/interfaces/options.interface.ts create mode 100644 libs/bootstrap/src/setups/cors.ts create mode 100644 libs/bootstrap/src/setups/index.ts create mode 100644 libs/bootstrap/src/setups/swagger.ts create mode 100644 libs/bootstrap/src/setups/throttler.ts create mode 100644 libs/bootstrap/tsconfig.lib.json diff --git a/.env.example b/.env.example index f981c6e..d3f2c1a 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # --- APP --- PORT=3000 NODE_ENV=development +CORS_ALLOWED_ORIGINS=http://localhost:3000 # --- POSTGRES --- DB_USERNAME=admin diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts new file mode 100644 index 0000000..80ec104 --- /dev/null +++ b/libs/bootstrap/src/bootstrap.ts @@ -0,0 +1,81 @@ +import { Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { NestFactory } from '@nestjs/core'; +import { setupThrottler } from './setups/throttler'; +import { DEFAULT_THROTTLER_OPTIONS } from './configs/throttler'; +import { setupCors, setupSwagger } from './setups'; +import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { BootstrapOptions } from './interfaces/options.interface'; +import fastifyCookie from '@fastify/cookie'; +import fastifyCompress from '@fastify/compress'; + +export async function bootstrapApp(options: BootstrapOptions) { + const adapter = new FastifyAdapter(); + + const { + appModule, + apiPrefix = 'api/v1', + serviceName = 'App', + portEnvKey = 'PORT', + defaultPort = 3000, + setupApp, + useCookieParser = true, + useCors = true, + throttlerOptions = DEFAULT_THROTTLER_OPTIONS, + swaggerOptions, + } = options; + + let rootModule = appModule; + + // TODO: Improve merging modules (in case of multiple features needed) + if (throttlerOptions) { + rootModule = setupThrottler(rootModule, throttlerOptions); + } + + const app = await NestFactory.create(rootModule, adapter, { + rawBody: true, + }); + const logger = new Logger(serviceName[0].toUpperCase() + serviceName.slice(1)); + const configService = app.get(ConfigService); + const port = configService.getOrThrow(portEnvKey, defaultPort); + const origins = configService.getOrThrow('CORS_ALLOWED_ORIGINS'); + + app.enableShutdownHooks(); + + await app.register(fastifyCompress, { + global: true, + threshold: 1024, + }); + + if (apiPrefix) app.setGlobalPrefix(apiPrefix); + if (useCors) setupCors(app, origins); + if (swaggerOptions) { + const { path = 'docs', ...metadata } = swaggerOptions; + + const domain = configService.get('DOMAIN'); + const stage = configService.get('STAGE_DOMAIN'); + + const fullOptions = { + ...metadata, + path, + server: { + port, + domain, + stage, + }, + }; + + await setupSwagger(app, fullOptions); + } + if (useCookieParser) app.register(fastifyCookie, { secret: 'SAME-SECRET' }); + if (setupApp) setupApp(app); + + await app.listen(port, '0.0.0.0', (_err, address) => { + if (_err) { + logger.error(_err); + process.exit(1); + } + + logger.verbose(`Application is running on: ${address}${apiPrefix ? '/' + apiPrefix : ''}`); + }); +} diff --git a/libs/bootstrap/src/configs/swagger.ts b/libs/bootstrap/src/configs/swagger.ts new file mode 100644 index 0000000..918911d --- /dev/null +++ b/libs/bootstrap/src/configs/swagger.ts @@ -0,0 +1,7 @@ +import type { SwaggerOptions } from '../interfaces/options.interface'; + +export const SWAGGER_DEFAULTS: SwaggerOptions = { + title: 'API', + description: 'API Documentation', + version: '1.0.0', +}; diff --git a/libs/bootstrap/src/configs/throttler.ts b/libs/bootstrap/src/configs/throttler.ts new file mode 100644 index 0000000..135f264 --- /dev/null +++ b/libs/bootstrap/src/configs/throttler.ts @@ -0,0 +1,9 @@ +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; + +export const DEFAULT_THROTTLER_OPTIONS: ThrottlerModuleOptions = [ + { + ttl: 60000, + limit: 100, + skipIf: (context) => context.getType() !== 'http', + }, +]; diff --git a/libs/bootstrap/src/index.ts b/libs/bootstrap/src/index.ts new file mode 100644 index 0000000..cbc96b3 --- /dev/null +++ b/libs/bootstrap/src/index.ts @@ -0,0 +1 @@ +export { bootstrapApp } from './bootstrap'; diff --git a/libs/bootstrap/src/interfaces/index.ts b/libs/bootstrap/src/interfaces/index.ts new file mode 100644 index 0000000..4d45ac8 --- /dev/null +++ b/libs/bootstrap/src/interfaces/index.ts @@ -0,0 +1 @@ +export type { BootstrapOptions, SwaggerOptions } from './options.interface'; diff --git a/libs/bootstrap/src/interfaces/options.interface.ts b/libs/bootstrap/src/interfaces/options.interface.ts new file mode 100644 index 0000000..8b2f22c --- /dev/null +++ b/libs/bootstrap/src/interfaces/options.interface.ts @@ -0,0 +1,35 @@ +import type { Config } from '@libs/config'; +import type { Type } from '@nestjs/common'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; + +export interface SwaggerMetadata { + title?: string; + description?: string; + version?: string; + path?: string; +} + +export interface SwaggerInfrastructure { + server?: { + port?: string | number; + domain?: string; + stage?: string; + }; + services?: { name: string; port: number }[]; +} + +export interface SwaggerOptions extends SwaggerMetadata, SwaggerInfrastructure {} + +export interface BootstrapOptions { + apiPrefix?: string; + appModule: Type; + defaultPort?: number; + portEnvKey?: keyof Config; + serviceName: string; + setupApp?: (app: NestFastifyApplication) => Promise | void; + swaggerOptions?: SwaggerMetadata; + throttlerOptions?: ThrottlerModuleOptions; + useCookieParser?: boolean; + useCors?: boolean; +} diff --git a/libs/bootstrap/src/setups/cors.ts b/libs/bootstrap/src/setups/cors.ts new file mode 100644 index 0000000..73d2847 --- /dev/null +++ b/libs/bootstrap/src/setups/cors.ts @@ -0,0 +1,27 @@ +import fastifyCors from '@fastify/cors'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; + +export function setupCors(app: NestFastifyApplication, origins: string[]) { + app.getHttpAdapter() + .getInstance() + .register(fastifyCors, { + origin: (origin, callback) => { + // server-to-server / curl / healthcheck + if (!origin) { + return callback(null, true); + } + + const { hostname } = new URL(origin); + + if (origins.some((o) => hostname === o || hostname.endsWith(`.${o}`))) { + callback(null, origin); + } + + callback(new Error('Not allowed by CORS'), false); + }, + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], + preflightContinue: false, + optionsSuccessStatus: 204, + }); +} diff --git a/libs/bootstrap/src/setups/index.ts b/libs/bootstrap/src/setups/index.ts new file mode 100644 index 0000000..2cfe699 --- /dev/null +++ b/libs/bootstrap/src/setups/index.ts @@ -0,0 +1,3 @@ +export { setupCors } from './cors'; +export { setupThrottler } from './throttler'; +export { setupSwagger } from './swagger'; diff --git a/libs/bootstrap/src/setups/swagger.ts b/libs/bootstrap/src/setups/swagger.ts new file mode 100644 index 0000000..90e938f --- /dev/null +++ b/libs/bootstrap/src/setups/swagger.ts @@ -0,0 +1,38 @@ +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { cleanupOpenApiDoc } from 'nestjs-zod'; +import type { NestFastifyApplication } from '@nestjs/platform-fastify'; +import type { SwaggerOptions } from '../interfaces'; +import { SWAGGER_DEFAULTS } from '../configs/swagger'; + +export async function setupSwagger(app: NestFastifyApplication, options: SwaggerOptions = {}) { + const { title, description, version, path, server } = { + ...SWAGGER_DEFAULTS, + ...options, + }; + + const { domain, port, stage } = server || {}; + + const builder = new DocumentBuilder() + .setTitle(title) + .setDescription(description) + .setVersion(version) + .addBearerAuth(); + + if (port) builder.addServer(`http://localhost:${port}`, 'Local'); + if (stage) builder.addServer(`https://api.${stage}`, 'Staging'); + if (domain) builder.addServer(`https://api.${domain}`, 'Production'); + + const document = SwaggerModule.createDocument(app, builder.build()); + + SwaggerModule.setup(path, app, cleanupOpenApiDoc(document), { + jsonDocumentUrl: `${path}/s/json`, + yamlDocumentUrl: `${path}/s/yaml`, + useGlobalPrefix: true, + ui: true, + swaggerOptions: { + persistAuthorization: true, + tagsSorter: 'alpha', + operationsSorter: 'alpha', + }, + }); +} diff --git a/libs/bootstrap/src/setups/throttler.ts b/libs/bootstrap/src/setups/throttler.ts new file mode 100644 index 0000000..59ac61a --- /dev/null +++ b/libs/bootstrap/src/setups/throttler.ts @@ -0,0 +1,19 @@ +import { Module, type Type } from '@nestjs/common'; +import type { ThrottlerModuleOptions } from '@nestjs/throttler'; +import { APP_GUARD } from '@nestjs/core'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; + +export function setupThrottler(module: Type, options: ThrottlerModuleOptions) { + @Module({ + imports: [module, ThrottlerModule.forRoot(options)], + providers: [ + { + provide: APP_GUARD, + useClass: ThrottlerGuard, + }, + ], + }) + class RootModule {} + + return RootModule; +} diff --git a/libs/bootstrap/tsconfig.lib.json b/libs/bootstrap/tsconfig.lib.json new file mode 100644 index 0000000..208ac7d --- /dev/null +++ b/libs/bootstrap/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/bootstrap" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 172e6b3..348a00f 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -10,6 +10,25 @@ export const ConfigSchema = z.object({ DATABASE_URL: z.string().url('DATABASE_URL must be a valid connection string'), REDIS_HOST: z.string().default('redis'), REDIS_PORT: z.coerce.number().default(6379), + DOMAIN: z + .string() + .toLowerCase() + .refine((val) => !val || /^[a-z0-9.-]+\.[a-z]{2,}$/.test(val), { + message: 'DOMAIN must be a valid hostname (e.g., example.com)', + }) + .optional(), + STAGE_DOMAIN: z + .string() + .toLowerCase() + .refine((val) => !val || /^[a-z0-9.-]+\.[a-z]{2,}$/.test(val), { + message: 'STAGE_DOMAIN must be a valid hostname', + }) + .optional(), + CORS_ALLOWED_ORIGINS: z + .string() + .min(1, "CORS_ALLOWED_ORIGINS can't be empty") + .transform((val) => val.split(',').map((s) => s.trim())) + .pipe(z.array(z.string().url('Each origin must be a valid URL'))), }); export type Config = z.infer; diff --git a/nest-cli.json b/nest-cli.json index 1079603..8daf27c 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -24,6 +24,15 @@ "compilerOptions": { "tsConfigPath": "libs/database/tsconfig.lib.json" } + }, + "bootstrap": { + "type": "library", + "root": "libs/bootstrap", + "entryFile": "index", + "sourceRoot": "libs/bootstrap/src", + "compilerOptions": { + "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" + } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 52f8530..1468528 100644 --- a/package.json +++ b/package.json @@ -1,68 +1,71 @@ { - "name": "task-backend", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "vitest run", - "test:watch": "vitest", - "test:cov": "vitest run --coverage", - "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false", - "test:e2e": "vitest run --config ./vitest.config.e2e.ts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@fastify/compress": "^8.3.1", - "@fastify/cookie": "^11.0.2", - "@nestjs/common": "^10.0.0", - "@nestjs/config": "^4.0.4", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-fastify": "^11.1.18", - "@nestjs/swagger": "^11.2.7", - "@nestjs/throttler": "^6.5.0", - "drizzle-orm": "^0.45.2", - "drizzle-zod": "^0.8.3", - "fastify": "^5.8.4", - "pg": "^8.20.0", - "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1", - "zod": "^4.3.6" - }, - "devDependencies": { - "@nestjs/cli": "^10.0.0", - "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^10.0.0", - "@types/node": "^20.3.1", - "@types/pg": "^8.20.0", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitest/coverage-v8": "^4.1.4", - "drizzle-kit": "^0.31.10", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "lint-staged": "^16.4.0", - "prettier": "^3.0.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3", - "unplugin-swc": "^1.5.9", - "vitest": "^4.1.4" - }, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" -} + "name": "task-backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "vitest run", + "test:watch": "vitest", + "test:cov": "vitest run --coverage", + "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false", + "test:e2e": "vitest run --config ./vitest.config.e2e.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@fastify/compress": "^8.3.1", + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.2.0", + "@fastify/static": "^9.1.0", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/platform-fastify": "^11.1.18", + "@nestjs/swagger": "^11.2.7", + "@nestjs/throttler": "^6.5.0", + "drizzle-orm": "^0.45.2", + "drizzle-zod": "^0.8.3", + "fastify": "^5.8.4", + "nestjs-zod": "^5.3.0", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.19", + "@nestjs/schematics": "^11.0.10", + "@nestjs/testing": "^11.1.18", + "@types/node": "^20.3.1", + "@types/pg": "^8.20.0", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitest/coverage-v8": "^4.1.4", + "drizzle-kit": "^0.31.10", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "lint-staged": "^16.4.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.4" + }, + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7b7109..3c4618f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,24 +14,30 @@ importers: '@fastify/cookie': specifier: ^11.0.2 version: 11.0.2 + '@fastify/cors': + specifier: ^11.2.0 + version: 11.2.0 + '@fastify/static': + specifier: ^9.1.0 + version: 9.1.0 '@nestjs/common': - specifier: ^10.0.0 - version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.4 - version: 4.0.4(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-fastify': specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) + version: 11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/swagger': specifier: ^11.2.7 - version: 11.2.7(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2) + version: 11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) '@nestjs/throttler': specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0) @@ -41,6 +47,9 @@ importers: fastify: specifier: ^5.8.4 version: 5.8.4 + nestjs-zod: + specifier: ^5.3.0 + version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) pg: specifier: ^8.20.0 version: 8.20.0 @@ -55,14 +64,14 @@ importers: version: 4.3.6 devDependencies: '@nestjs/cli': - specifier: ^10.0.0 - version: 10.4.9(@swc/core@1.15.24)(esbuild@0.27.7) + specifier: ^11.0.19 + version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) '@nestjs/schematics': - specifier: ^10.0.0 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + specifier: ^11.0.10 + version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': - specifier: ^10.0.0 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@types/node': specifier: ^20.3.1 version: 20.19.39 @@ -107,7 +116,7 @@ importers: version: 6.3.4 ts-loader: specifier: ^9.4.3 - version: 9.5.7(typescript@5.9.3)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)) + version: 9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) ts-node: specifier: ^10.9.1 version: 10.9.2(@swc/core@1.15.24)(@types/node@20.19.39)(typescript@5.9.3) @@ -126,23 +135,36 @@ importers: packages: - '@angular-devkit/core@17.3.11': - resolution: {integrity: sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/core@19.2.23': + resolution: {integrity: sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: - chokidar: ^3.5.2 + chokidar: ^4.0.0 peerDependenciesMeta: chokidar: optional: true - '@angular-devkit/schematics-cli@17.3.11': - resolution: {integrity: sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics-cli@19.2.24': + resolution: {integrity: sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@17.3.11': - resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} - engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@19.2.23': + resolution: {integrity: sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -695,6 +717,12 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@fastify/send@4.1.0': + resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} + + '@fastify/static@9.1.0': + resolution: {integrity: sha512-EPRNQYqEYEYTK8yyGbcM0iHpyJaupb94bey5O6iCQfLTADr02kaZU+qeHSdd9H9TiMwTBVkrMa59V8CMbn3avQ==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -708,9 +736,148 @@ packages: resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} deprecated: Use @eslint/object-schema instead - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -734,14 +901,14 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@ljharb/through@2.3.14': - resolution: {integrity: sha512-ajBvlKpWucBB17FuQYUShqpqy8GRgYEpJW0vWJbUu1CV9lWyrDCapy0lScU8T8Z6qn49sSwJB3+M+evYIdGg+A==} - engines: {node: '>= 0.4'} - '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} @@ -751,12 +918,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@nestjs/cli@10.4.9': - resolution: {integrity: sha512-s8qYd97bggqeK7Op3iD49X2MpFtW4LVNLAwXFkfbRxKME6IYT7X0muNTJ2+QfI8hpbNx9isWkrLWIp+g5FOhiA==} - engines: {node: '>= 16.14'} + '@nestjs/cli@11.0.19': + resolution: {integrity: sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==} + engines: {node: '>= 20.11'} hasBin: true peerDependencies: - '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 '@swc/core': ^1.3.62 peerDependenciesMeta: '@swc/cli': @@ -764,11 +931,11 @@ packages: '@swc/core': optional: true - '@nestjs/common@10.4.22': - resolution: {integrity: sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==} + '@nestjs/common@11.1.18': + resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} peerDependencies: - class-transformer: '*' - class-validator: '*' + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -783,13 +950,14 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@10.4.22': - resolution: {integrity: sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==} + '@nestjs/core@11.1.18': + resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} + engines: {node: '>= 20'} peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - '@nestjs/websockets': ^10.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -813,12 +981,6 @@ packages: class-validator: optional: true - '@nestjs/platform-express@10.4.22': - resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==} - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 - '@nestjs/platform-fastify@11.1.18': resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} peerDependencies: @@ -832,8 +994,8 @@ packages: '@fastify/view': optional: true - '@nestjs/schematics@10.2.3': - resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==} + '@nestjs/schematics@11.0.10': + resolution: {integrity: sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==} peerDependencies: typescript: '>=4.8.2' @@ -854,13 +1016,13 @@ packages: class-validator: optional: true - '@nestjs/testing@10.4.22': - resolution: {integrity: sha512-HO9aPus3bAedAC+jKVAA8jTdaj4fs5M9fing4giHrcYV2txe9CvC1l1WAjwQ9RDhEHdugjY4y+FZA/U/YqPZrA==} + '@nestjs/testing@11.1.18': + resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 peerDependenciesMeta: '@nestjs/microservices': optional: true @@ -890,9 +1052,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nuxtjs/opencollective@0.3.2': - resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true '@oxc-project/types@0.124.0': @@ -904,10 +1066,6 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1118,8 +1276,8 @@ packages: '@swc/types@0.1.26': resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} '@tokenizer/token@0.3.0': @@ -1336,9 +1494,11 @@ packages: abstract-logging@2.0.1: resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1383,9 +1543,6 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} - ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -1393,10 +1550,6 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -1417,12 +1570,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1430,9 +1580,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1463,6 +1610,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1471,23 +1622,19 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} brace-expansion@2.0.3: resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1506,22 +1653,10 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -1541,16 +1676,12 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} @@ -1576,10 +1707,6 @@ packages: resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} engines: {node: '>=20'} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1613,8 +1740,8 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - comment-json@4.2.5: - resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} component-emitter@1.3.1: @@ -1623,31 +1750,17 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - - consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1658,10 +1771,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -1678,14 +1787,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1705,10 +1806,6 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1721,10 +1818,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1868,12 +1961,6 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} @@ -1883,13 +1970,6 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -1912,9 +1992,6 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -1948,10 +2025,6 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2029,10 +2102,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -2048,14 +2117,6 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} - engines: {node: '>= 0.10.0'} - - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -2105,29 +2166,18 @@ packages: picomatch: optional: true - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - file-type@20.4.1: - resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} - engines: {node: '>=18'} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} - find-my-way@9.5.0: resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} engines: {node: '>=20'} @@ -2143,13 +2193,9 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - fork-ts-checker-webpack-plugin@9.0.2: - resolution: {integrity: sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==} - engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} peerDependencies: typescript: '>3.6.0' webpack: ^5.11.0 @@ -2161,14 +2207,6 @@ packages: formidable@2.1.5: resolution: {integrity: sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -2213,10 +2251,9 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -2244,13 +2281,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-own-prop@2.0.0: - resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} - engines: {node: '>=8'} - - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2270,8 +2300,8 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} ieee754@1.2.1: @@ -2296,18 +2326,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - - inquirer@9.2.15: - resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} - engines: {node: '>=18'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2315,10 +2333,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2373,9 +2387,6 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -2413,9 +2424,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonc-parser@3.2.1: - resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -2518,6 +2526,10 @@ packages: resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} engines: {node: '>=20.0.0'} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2544,16 +2556,16 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.3: + resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + engines: {node: 20 || >=22} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} - magicast@0.5.2: resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} @@ -2568,17 +2580,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2602,16 +2607,16 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} engines: {node: '>=4.0.0'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2620,6 +2625,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -2627,10 +2636,6 @@ packages: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -2638,26 +2643,12 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.0.2: - resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} - engines: {node: '>= 10.16.0'} - - mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - - mute-stream@1.0.0: - resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} @@ -2667,39 +2658,29 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-zod@5.3.0: + resolution: {integrity: sha512-QY6imXm9heMOpWigjFHgMWPvc1ZQHeNQ7pdogo9Q5xj5F8HpqZ972vKlVdkaTyzYlOXJP/yVy3wlF1EjubDQPg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2711,10 +2692,6 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2734,10 +2711,6 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2746,9 +2719,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2757,10 +2727,6 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2773,15 +2739,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - - path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -2837,10 +2797,6 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.1: - resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} - engines: {node: '>=12'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -2905,10 +2861,6 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -2919,10 +2871,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} - qs@6.15.1: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} @@ -2933,14 +2881,6 @@ packages: quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -2952,9 +2892,9 @@ packages: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} @@ -2963,10 +2903,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3007,14 +2943,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - - run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3057,21 +2985,9 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} - set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -3160,10 +3076,6 @@ packages: stream-shift@1.0.3: resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3172,10 +3084,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3274,9 +3182,6 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3292,10 +3197,6 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3312,13 +3213,6 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - ts-api-utils@1.4.3: resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} @@ -3370,22 +3264,6 @@ packages: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - - typescript@5.7.2: - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -3406,10 +3284,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin-swc@1.5.9: resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} peerDependencies: @@ -3431,17 +3305,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite@8.0.8: resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3533,9 +3399,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-node-externals@3.0.0: resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} engines: {node: '>=6'} @@ -3547,8 +3410,8 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - webpack@5.97.1: - resolution: {integrity: sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==} + webpack@5.106.0: + resolution: {integrity: sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -3557,9 +3420,6 @@ packages: webpack-cli: optional: true - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3578,14 +3438,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3614,38 +3466,64 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: - '@angular-devkit/core@17.3.11(chokidar@3.6.0)': + '@angular-devkit/core@19.2.23(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': dependencies: - ajv: 8.12.0 - ajv-formats: 2.1.1(ajv@8.12.0) - jsonc-parser: 3.2.1 - picomatch: 4.0.1 + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 rxjs: 7.8.1 source-map: 0.7.4 optionalDependencies: - chokidar: 3.6.0 + chokidar: 4.0.3 - '@angular-devkit/schematics-cli@17.3.11(chokidar@3.6.0)': + '@angular-devkit/schematics-cli@19.2.24(@types/node@20.19.39)(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@20.19.39) ansi-colors: 4.1.3 - inquirer: 9.2.15 symbol-observable: 4.0.0 yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.23(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 transitivePeerDependencies: - chokidar - '@angular-devkit/schematics@17.3.11(chokidar@3.6.0)': + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - jsonc-parser: 3.2.1 - magic-string: 0.30.8 + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 ora: 5.4.1 rxjs: 7.8.1 transitivePeerDependencies: @@ -4005,6 +3883,23 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.3.0 + '@fastify/send@4.1.0': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.1 + mime: 3.0.0 + + '@fastify/static@9.1.0': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 4.1.0 + content-disposition: 1.1.0 + fastify-plugin: 5.1.0 + fastq: 1.20.1 + glob: 13.0.6 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4017,14 +3912,145 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@isaacs/cliui@8.0.2': + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@20.19.39)': dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/confirm@5.1.21(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/core@10.3.2(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/editor@4.2.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/external-editor': 1.0.3(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/expand@4.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/external-editor@1.0.3(@types/node@20.19.39)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/number@3.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/password@4.0.23(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/prompts@7.10.1(@types/node@20.19.39)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) + '@inquirer/confirm': 5.1.21(@types/node@20.19.39) + '@inquirer/editor': 4.2.23(@types/node@20.19.39) + '@inquirer/expand': 4.0.23(@types/node@20.19.39) + '@inquirer/input': 4.3.1(@types/node@20.19.39) + '@inquirer/number': 3.0.23(@types/node@20.19.39) + '@inquirer/password': 4.0.23(@types/node@20.19.39) + '@inquirer/rawlist': 4.1.11(@types/node@20.19.39) + '@inquirer/search': 3.2.2(@types/node@20.19.39) + '@inquirer/select': 4.4.2(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/prompts@7.3.2(@types/node@20.19.39)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@20.19.39) + '@inquirer/confirm': 5.1.21(@types/node@20.19.39) + '@inquirer/editor': 4.2.23(@types/node@20.19.39) + '@inquirer/expand': 4.0.23(@types/node@20.19.39) + '@inquirer/input': 4.3.1(@types/node@20.19.39) + '@inquirer/number': 3.0.23(@types/node@20.19.39) + '@inquirer/password': 4.0.23(@types/node@20.19.39) + '@inquirer/rawlist': 4.1.11(@types/node@20.19.39) + '@inquirer/search': 3.2.2(@types/node@20.19.39) + '@inquirer/select': 4.4.2(@types/node@20.19.39) + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/rawlist@4.1.11(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/search@3.2.2(@types/node@20.19.39)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/select@4.4.2(@types/node@20.19.39)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@20.19.39) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@20.19.39) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 20.19.39 + + '@inquirer/type@3.0.10(@types/node@20.19.39)': + optionalDependencies: + '@types/node': 20.19.39 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -4055,12 +4081,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@ljharb/through@2.3.14': - dependencies: - call-bind: 1.0.9 - '@lukeed/csprng@1.1.0': {} + '@lukeed/ms@2.0.2': {} + '@microsoft/tsdoc@0.16.0': {} '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': @@ -4070,38 +4094,39 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nestjs/cli@10.4.9(@swc/core@1.15.24)(esbuild@0.27.7)': + '@nestjs/cli@11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7)': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.24(@types/node@20.19.39)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@20.19.39) + '@nestjs/schematics': 11.0.10(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)) - glob: 10.4.5 - inquirer: 8.2.6 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) + glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 - tree-kill: 1.2.2 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1(@swc/core@1.15.24)(esbuild@0.27.7) + typescript: 5.9.3 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.15.24 transitivePeerDependencies: + - '@types/node' - esbuild - uglify-js - webpack-cli - '@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 20.4.1 + file-type: 21.3.4 iterare: 1.2.1 + load-esm: 1.0.3 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 @@ -4109,54 +4134,37 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.4(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 17.4.1 dotenv-expand: 12.0.3 lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nuxtjs/opencollective': 0.3.2 + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 - path-to-regexp: 3.3.0 + path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 - optionalDependencies: - '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) - transitivePeerDependencies: - - encoding - '@nestjs/mapped-types@2.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': - dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) - body-parser: 1.20.4 - cors: 2.8.5 - express: 4.22.1 - multer: 2.0.2 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - optional: true - - '@nestjs/platform-fastify@11.1.18(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': + '@nestjs/platform-fastify@11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@fastify/cors': 11.2.0 '@fastify/formbody': 8.0.2 - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 fastify: 5.8.4 fastify-plugin: 5.1.0 @@ -4165,53 +4173,44 @@ snapshots: path-to-regexp: 8.4.2 reusify: 1.1.0 tslib: 2.8.1 + optionalDependencies: + '@fastify/static': 9.1.0 - '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.7.2)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - comment-json: 4.2.5 - jsonc-parser: 3.3.1 - pluralize: 8.0.0 - typescript: 5.7.2 - transitivePeerDependencies: - - chokidar - - '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.9.3)': + '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - comment-json: 4.2.5 + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + comment-json: 4.6.2 jsonc-parser: 3.3.1 pluralize: 8.0.0 typescript: 5.9.3 transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.2.7(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) js-yaml: 4.1.1 lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 swagger-ui-dist: 5.32.2 + optionalDependencies: + '@fastify/static': 9.1.0 - '@nestjs/testing@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/platform-express@10.4.22)': + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - optionalDependencies: - '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22) - '@nestjs/throttler@6.5.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 '@noble/hashes@1.8.0': {} @@ -4228,13 +4227,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@nuxtjs/opencollective@0.3.2': + '@nuxt/opencollective@0.4.1': dependencies: - chalk: 4.1.2 - consola: 2.15.3 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding + consola: 3.4.2 '@oxc-project/types@0.124.0': {} @@ -4244,9 +4239,6 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@pkgjs/parseargs@0.11.0': - optional: true - '@pkgr/core@0.2.9': {} '@rolldown/binding-android-arm64@1.0.0-rc.15': @@ -4370,10 +4362,9 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tokenizer/inflate@0.2.7': + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 - fflate: 0.8.2 token-types: 6.1.2 transitivePeerDependencies: - supports-color @@ -4671,11 +4662,9 @@ snapshots: abstract-logging@2.0.1: {} - accepts@1.3.8: + acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - optional: true + acorn: 8.16.0 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -4687,10 +4676,6 @@ snapshots: acorn@8.16.0: {} - ajv-formats@2.1.1(ajv@8.12.0): - optionalDependencies: - ajv: 8.12.0 - ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -4715,13 +4700,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ajv@8.12.0: - dependencies: - fast-deep-equal: 3.1.3 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - uri-js: 4.4.1 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -4731,10 +4709,6 @@ snapshots: ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -4749,21 +4723,12 @@ snapshots: ansi-styles@6.2.3: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - append-field@1.0.0: - optional: true + ansis@4.2.0: {} arg@4.1.3: {} argparse@2.0.1: {} - array-flatten@1.1.1: - optional: true - array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -4789,36 +4754,18 @@ snapshots: balanced-match@1.0.2: {} + balanced-match@4.0.4: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.10.17: {} - binary-extensions@2.3.0: {} - bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.4: - 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.2 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - optional: true - brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -4828,6 +4775,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -4852,26 +4803,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - busboy@1.6.0: - dependencies: - streamsearch: 1.1.0 - optional: true - - bytes@3.1.2: - optional: true - call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4888,21 +4824,11 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} - - chardet@0.7.0: {} + chardet@2.1.1: {} - chokidar@3.6.0: + chokidar@4.0.3: dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + readdirp: 4.1.2 chrome-trace-event@1.0.4: {} @@ -4927,8 +4853,6 @@ snapshots: slice-ansi: 8.0.0 string-width: 8.2.0 - cli-width@3.0.0: {} - cli-width@4.1.0: {} clone@1.0.4: {} @@ -4951,64 +4875,35 @@ snapshots: commander@4.1.1: {} - comment-json@4.2.5: + comment-json@4.6.2: dependencies: array-timsort: 1.0.3 - core-util-is: 1.0.3 esprima: 4.0.1 - has-own-prop: 2.0.0 - repeat-string: 1.6.1 component-emitter@1.3.1: {} concat-map@0.0.1: {} - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - optional: true - - consola@2.15.3: {} + consola@3.4.2: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - optional: true - - content-type@1.0.5: - optional: true + content-disposition@1.1.0: {} convert-source-map@2.0.0: {} - cookie-signature@1.0.7: - optional: true - - cookie@0.7.2: - optional: true - cookie@1.1.1: {} cookiejar@2.1.4: {} core-util-is@1.0.3: {} - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - optional: true - - cosmiconfig@8.3.6(typescript@5.7.2): + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.1 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.7.2 + typescript: 5.9.3 create-require@1.1.1: {} @@ -5018,11 +4913,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - debug@2.6.9: - dependencies: - ms: 2.0.0 - optional: true - debug@4.4.3: dependencies: ms: 2.1.3 @@ -5035,22 +4925,12 @@ snapshots: dependencies: clone: 1.0.4 - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - delayed-stream@1.0.0: {} - depd@2.0.0: - optional: true + depd@2.0.0: {} dequal@2.0.3: {} - destroy@1.2.0: - optional: true - detect-libc@2.1.2: {} dezalgo@1.0.4: @@ -5113,22 +4993,12 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 - eastasianwidth@0.2.0: {} - - ee-first@1.1.1: - optional: true - electron-to-chromium@1.5.334: {} emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} - emoji-regex@9.2.2: {} - - encodeurl@2.0.0: - optional: true - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -5148,8 +5018,6 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -5248,10 +5116,7 @@ snapshots: escalade@3.2.0: {} - escape-html@1.0.3: - optional: true - - escape-string-regexp@1.0.5: {} + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -5352,9 +5217,6 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: - optional: true - event-target-shim@5.0.1: {} eventemitter3@5.0.4: {} @@ -5363,49 +5225,6 @@ snapshots: expect-type@1.3.0: {} - express@4.22.1: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.4 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.13 - proxy-addr: 2.0.7 - qs: 6.14.2 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - optional: true - - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -5469,19 +5288,13 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - fflate@0.8.2: {} - - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - file-entry-cache@6.0.1: dependencies: flat-cache: 3.2.0 - file-type@20.4.1: + file-type@21.3.4: dependencies: - '@tokenizer/inflate': 0.2.7 + '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 @@ -5492,19 +5305,6 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.2: - 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 - transitivePeerDependencies: - - supports-color - optional: true - find-my-way@9.5.0: dependencies: fast-deep-equal: 3.1.3 @@ -5524,17 +5324,12 @@ snapshots: flatted@3.4.2: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) deepmerge: 4.3.1 fs-extra: 10.1.0 memfs: 3.5.3 @@ -5543,8 +5338,8 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.4 tapable: 2.3.2 - typescript: 5.7.2 - webpack: 5.97.1(@swc/core@1.15.24)(esbuild@0.27.7) + typescript: 5.9.3 + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) form-data@4.0.5: dependencies: @@ -5561,12 +5356,6 @@ snapshots: once: 1.4.0 qs: 6.15.1 - forwarded@0.2.0: - optional: true - - fresh@0.5.2: - optional: true - fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -5616,14 +5405,11 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.4.5: + glob@13.0.6: dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 + minimatch: 10.2.5 minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 + path-scurry: 2.0.2 glob@7.2.3: dependencies: @@ -5655,12 +5441,6 @@ snapshots: has-flag@4.0.0: {} - has-own-prop@2.0.0: {} - - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -5680,9 +5460,8 @@ snapshots: setprototypeof: 1.2.0 statuses: 2.0.2 toidentifier: 1.0.1 - optional: true - iconv-lite@0.4.24: + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5704,53 +5483,10 @@ snapshots: inherits@2.0.4: {} - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.18.1 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - - inquirer@9.2.15: - dependencies: - '@ljharb/through': 2.3.14 - ansi-escapes: 4.3.2 - chalk: 5.6.2 - cli-cursor: 3.1.0 - cli-width: 4.1.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.18.1 - mute-stream: 1.0.0 - ora: 5.4.1 - run-async: 3.0.0 - rxjs: 7.8.2 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - - ipaddr.js@1.9.1: - optional: true - ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -5790,12 +5526,6 @@ snapshots: iterare@1.2.1: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jest-worker@27.5.1: dependencies: '@types/node': 20.19.39 @@ -5826,8 +5556,6 @@ snapshots: json5@2.2.3: {} - jsonc-parser@3.2.1: {} - jsonc-parser@3.3.1: {} jsonfile@6.2.0: @@ -5920,6 +5648,8 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + load-esm@1.0.3: {} + load-tsconfig@0.2.5: {} loader-runner@4.3.1: {} @@ -5945,13 +5675,13 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - lru-cache@10.4.3: {} + lru-cache@11.3.3: {} - magic-string@0.30.21: + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magic-string@0.30.8: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5969,16 +5699,10 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@0.3.0: - optional: true - memfs@3.5.3: dependencies: fs-monkey: 1.1.0 - merge-descriptors@1.0.3: - optional: true - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5996,15 +5720,18 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: - optional: true - mime@2.6.0: {} + mime@3.0.0: {} + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + minimatch@3.1.5: dependencies: brace-expansion: 1.1.13 @@ -6013,76 +5740,43 @@ snapshots: dependencies: brace-expansion: 2.0.3 - minimatch@9.0.9: - dependencies: - brace-expansion: 2.0.3 - minimist@1.2.8: {} minipass@7.1.3: {} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - optional: true - - ms@2.0.0: - optional: true - ms@2.1.3: {} - multer@2.0.2: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - mkdirp: 0.5.6 - object-assign: 4.1.1 - type-is: 1.6.18 - xtend: 4.0.2 - optional: true - - mute-stream@0.0.8: {} - - mute-stream@1.0.0: {} + mute-stream@2.0.0: {} nanoid@3.3.11: {} natural-compare@1.4.0: {} - negotiator@0.6.3: - optional: true - neo-async@2.6.2: {} + nestjs-zod@5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + optionalDependencies: + '@nestjs/swagger': 11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + node-abort-controller@3.1.1: {} node-emoji@1.11.0: dependencies: lodash: 4.18.1 - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - node-releases@2.0.37: {} - normalize-path@3.0.0: {} - - object-assign@4.1.1: - optional: true - object-inspect@1.13.4: {} obug@2.1.1: {} on-exit-leak-free@2.1.2: {} - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - optional: true - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6116,8 +5810,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - os-tmpdir@1.0.2: {} - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -6126,8 +5818,6 @@ snapshots: dependencies: p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -6139,25 +5829,17 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - parseurl@1.3.3: - optional: true - path-exists@4.0.0: {} path-is-absolute@1.0.1: {} path-key@3.1.1: {} - path-scurry@1.11.1: + path-scurry@2.0.2: dependencies: - lru-cache: 10.4.3 + lru-cache: 11.3.3 minipass: 7.1.3 - path-to-regexp@0.1.13: - optional: true - - path-to-regexp@3.3.0: {} - path-to-regexp@8.4.2: {} path-type@4.0.0: {} @@ -6209,8 +5891,6 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.1: {} - picomatch@4.0.4: {} pino-abstract-transport@3.0.0: @@ -6267,12 +5947,6 @@ snapshots: process@0.11.10: {} - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - optional: true - pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -6286,11 +5960,6 @@ snapshots: punycode@2.3.1: {} - qs@6.14.2: - dependencies: - side-channel: 1.1.0 - optional: true - qs@6.15.1: dependencies: side-channel: 1.1.0 @@ -6299,17 +5968,6 @@ snapshots: quick-format-unescaped@4.0.4: {} - range-parser@1.2.1: - optional: true - - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - optional: true - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -6334,16 +5992,12 @@ snapshots: process: 0.11.10 string_decoder: 1.3.0 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 + readdirp@4.1.2: {} real-require@0.2.0: {} reflect-metadata@0.2.2: {} - repeat-string@1.6.1: {} - require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -6391,10 +6045,6 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - run-async@2.4.1: {} - - run-async@3.0.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -6436,48 +6086,9 @@ snapshots: semver@7.7.4: {} - send@0.19.2: - 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 - transitivePeerDependencies: - - supports-color - optional: true - - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - optional: true - set-cookie-parser@2.7.2: {} - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - setprototypeof@1.2.0: - optional: true + setprototypeof@1.2.0: {} shebang-command@2.0.0: dependencies: @@ -6552,16 +6163,12 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.2: - optional: true + statuses@2.0.2: {} std-env@4.0.0: {} stream-shift@1.0.3: {} - streamsearch@1.1.0: - optional: true - string-argv@0.3.2: {} string-width@4.2.3: @@ -6570,12 +6177,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - string-width@7.2.0: dependencies: emoji-regex: 10.6.0 @@ -6653,13 +6254,13 @@ snapshots: tapable@2.3.2: {} - terser-webpack-plugin@5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)): + terser-webpack-plugin@5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.46.1 - webpack: 5.97.1(@swc/core@1.15.24)(esbuild@0.27.7) + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) optionalDependencies: '@swc/core': 1.15.24 esbuild: 0.27.7 @@ -6682,8 +6283,6 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 - through@2.3.8: {} - tinybench@2.9.0: {} tinyexec@1.1.1: {} @@ -6695,18 +6294,13 @@ snapshots: tinyrainbow@3.1.0: {} - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 toad-cache@3.7.0: {} - toidentifier@1.0.1: - optional: true + toidentifier@1.0.1: {} token-types@6.1.2: dependencies: @@ -6714,15 +6308,11 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tr46@0.0.3: {} - - tree-kill@1.2.2: {} - ts-api-utils@1.4.3(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-loader@9.5.7(typescript@5.9.3)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)): + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: chalk: 4.1.2 enhanced-resolve: 5.20.1 @@ -6730,7 +6320,7 @@ snapshots: semver: 7.7.4 source-map: 0.7.6 typescript: 5.9.3 - webpack: 5.97.1(@swc/core@1.15.24)(esbuild@0.27.7) + webpack: 5.106.0(@swc/core@1.15.24)(esbuild@0.27.7) ts-node@10.9.2(@swc/core@1.15.24)(@types/node@20.19.39)(typescript@5.9.3): dependencies: @@ -6780,19 +6370,6 @@ snapshots: type-fest@0.20.2: {} - type-fest@0.21.3: {} - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - optional: true - - typedarray@0.0.6: - optional: true - - typescript@5.7.2: {} - typescript@5.9.3: {} uid@2.0.2: @@ -6805,9 +6382,6 @@ snapshots: universalify@2.0.1: {} - unpipe@1.0.0: - optional: true - unplugin-swc@1.5.9(@swc/core@1.15.24): dependencies: '@rollup/pluginutils': 5.3.0 @@ -6836,14 +6410,8 @@ snapshots: util-deprecate@1.0.2: {} - utils-merge@1.0.1: - optional: true - v8-compile-cache-lib@3.0.1: {} - vary@1.1.2: - optional: true - vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -6896,26 +6464,26 @@ snapshots: dependencies: defaults: 1.0.4 - webidl-conversions@3.0.1: {} - webpack-node-externals@3.0.0: {} webpack-sources@3.3.4: {} webpack-virtual-modules@0.6.2: {} - webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7): + webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.20.1 - es-module-lexer: 1.7.0 + es-module-lexer: 2.0.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -6924,9 +6492,9 @@ snapshots: loader-runner: 4.3.1 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 3.3.0 + schema-utils: 4.3.3 tapable: 2.3.2 - terser-webpack-plugin: 5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.97.1(@swc/core@1.15.24)(esbuild@0.27.7)) + terser-webpack-plugin: 5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -6934,11 +6502,6 @@ snapshots: - esbuild - uglify-js - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -6956,18 +6519,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -6986,4 +6537,6 @@ snapshots: yocto-queue@0.1.0: {} + yoctocolors-cjs@2.1.3: {} + zod@4.3.6: {} diff --git a/src/main.ts b/src/main.ts index df101b2..aa7c5a0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,18 @@ -import { NestFactory } from '@nestjs/core'; +import { bootstrapApp } from '@libs/bootstrap'; import { AppModule } from './app.module'; -import { ConfigService } from '@nestjs/config'; -async function bootstrap() { - const app = await NestFactory.create(AppModule); - - const config = app.get(ConfigService); - const port = config.getOrThrow('PORT'); - - await app.listen(port); -} - -bootstrap(); +bootstrapApp({ + serviceName: 'Tracker Monolit', + appModule: AppModule, + apiPrefix: 'api/v1', + defaultPort: 2000, + portEnvKey: 'PORT', + swaggerOptions: { + title: 'Task Tracker API', + description: 'API бэкенда таск-трекера', + version: '0.1.0', + path: 'ui', + }, + useCors: true, + useCookieParser: true, +}); diff --git a/tsconfig.json b/tsconfig.json index d798e4f..503f4c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,9 @@ "@libs/config": ["./libs/config/src"], "@libs/config/*": ["./libs/config/src/*"], "@libs/database": ["./libs/database/src"], - "@libs/database/*": ["./libs/database/src/*"] + "@libs/database/*": ["./libs/database/src/*"], + "@libs/bootstrap": ["./libs/bootstrap/src"], + "@libs/bootstrap/*": ["./libs/bootstrap/src/*"] } }, "include": [ From 330ee741200346ecb7cd66dc47d7536c028dfb65 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 21:49:35 +0300 Subject: [PATCH 05/47] feat(bootstrap): implement zod ecosystem contracts --- src/app.module.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app.module.ts b/src/app.module.ts index 74ca97c..0f9f1cf 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,8 @@ import { ConfigModule } from '@libs/config'; import { DatabaseModule } from '@libs/database'; import { ConfigService } from '@nestjs/config'; import * as schema from './shared/entities'; +import { APP_FILTER, APP_PIPE } from '@nestjs/core'; +import { ZodValidationPipe, ZodValidationException } from 'nestjs-zod'; @Module({ imports: [ @@ -22,6 +24,16 @@ import * as schema from './shared/entities'; }), ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_PIPE, + useClass: ZodValidationPipe, + }, + { + provide: APP_FILTER, + useClass: ZodValidationException, + }, + ], }) export class AppModule {} From ae98bf2461ef4d881d6c3886f93d911f9f77fbea Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 22:29:51 +0300 Subject: [PATCH 06/47] ci: add docker build check workflow --- .github/workflows/build.yml | 49 +++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..00f5246 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +name: Build and Push + +on: + push: + branches: [dev, main, feat/**] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-push: + runs-on: ubuntu-latest + permissions: + contents: read + # packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # - name: Log in to the Container registry + # uses: docker/login-action@v3 + # with: + # registry: ${{ env.REGISTRY }} + # username: ${{ github.actor }} + # password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha,format=short + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.prod + push: false # add true, if your setup variables + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4569ae1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + pull_request: + branches: [dev, main, "feat/**"] + push: + branches: [dev, main, "feat/**"] + +jobs: + quality-check: + name: Lint & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run Lint + run: pnpm run lint + + - name: Type Check + run: pnpm exec tsc --noEmit + + - name: Run Tests + run: pnpm run test From 656beece80890352e72810abfbc51c1f1da8c760 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 22:45:02 +0300 Subject: [PATCH 07/47] docs(gh): setup issue templates and community configuration --- .github/ISSUE_TEMPLATE/bug_report.yml | 41 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 10 ++++++ .github/ISSUE_TEMPLATE/feature_request.yml | 18 ++++++++++ .github/workflows/build.yml | 2 +- .github/workflows/ci.yml | 4 +-- .github/workflows/codeql.yml | 33 +++++++++++++++++ .github/workflows/release-please.yml | 18 ++++++++++ .github/workflows/stale.yml | 19 ++++++++++ 8 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/stale.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..f166041 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,41 @@ +name: "Bug Report" +description: "Сообщить об ошибке в работе приложения" +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Спасибо, что решили помочь сделать проект лучше! + - type: input + id: version + attributes: + label: "Версия приложения" + description: "Какую версию вы используете? (например, 0.0.1)" + placeholder: "0.0.x" + validations: + required: true + - type: textarea + id: steps + attributes: + label: "Шаги воспроизведения" + description: "Как нам увидеть эту ошибку?" + placeholder: | + 1. Запустить docker-compose + 2. Отправить POST запрос на /api/v1/auth... + validations: + required: true + - type: dropdown + id: environment + attributes: + label: "Окружение" + options: + - Docker + - Local (pnpm) + - Production + validations: + required: true + - type: textarea + id: expected + attributes: + label: "Ожидаемое поведение" + placeholder: "Что должно было произойти?" \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..656e5e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false + +contact_links: + - name: "❓ Вопросы по использованию" + url: "https://github.com/Task-Tracker-Lab/task-tracker-backend/discussions/new?category=q-a" + about: "Если вы не уверены, баг это или нет, или вам нужна помощь в настройке — спросите здесь." + + - name: "💡 Идеи и предложения" + url: "https://github.com/Task-Tracker-Lab/task-tracker-backend/discussions/new?category=ideas" + about: "Хотите обсудить новую крутую фичу перед тем, как заводить задачу? Вам сюда." \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..4c6bacc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,18 @@ +name: "🚀 Feature Request" +description: "Предложить новую идею или улучшение" +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: "Какую проблему мы решаем?" + description: "Опишите, почему текущего функционала недостаточно." + validations: + required: true + - type: textarea + id: solution + attributes: + label: "Ваше предложение" + description: "Как именно вы видите реализацию этой фичи?" + validations: + required: true \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 00f5246..9b1744f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build and Push on: push: - branches: [dev, main, feat/**] + branches: [dev, main] env: REGISTRY: ghcr.io diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4569ae1..aa9a576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: pull_request: - branches: [dev, main, "feat/**"] + branches: [dev, main] push: - branches: [dev, main, "feat/**"] + branches: [dev, main] jobs: quality-check: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c168cd6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: "CodeQL" + +on: + push: + branches: [main, dev, feat/**, chore/**, build/**] + pull_request: + branches: [main] + schedule: + - cron: "15 13 * * 5" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..95b126c --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,18 @@ +name: release-please + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + release-type: node diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..fbc62d1 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: "Close stale issues and PRs" + +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "Эта задача давно не обновлялась. Она будет закрыта через 5 дней, если не появится новой активности." + stale-pr-message: "Этот PR замер. Мы закроем его через 5 дней, чтобы не копить очередь, но вы всегда можете переоткрыть его позже." + days-before-stale: 30 + days-before-close: 5 From 4e56a09648d314b9878b68daacfbfb8fb1f3dcba Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:12:40 +0300 Subject: [PATCH 08/47] feat: implement observability stack prometheus --- package.json | 1 + pnpm-lock.yaml | 59 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 1468528..e6212c8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/platform-fastify": "^11.1.18", "@nestjs/swagger": "^11.2.7", "@nestjs/throttler": "^6.5.0", + "@willsoto/nestjs-prometheus": "^6.1.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "fastify": "^5.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c4618f..1b41984 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,12 +38,15 @@ importers: '@nestjs/throttler': specifier: ^6.5.0 version: 6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@willsoto/nestjs-prometheus': + specifier: ^6.1.0 + version: 6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(@types/pg@8.20.0)(pg@8.20.0) + version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) drizzle-zod: specifier: ^0.8.3 - version: 0.8.3(drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6) + version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6) fastify: specifier: ^5.8.4 version: 5.8.4 @@ -131,7 +134,7 @@ importers: version: 1.5.9(@swc/core@1.15.24) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -1057,6 +1060,10 @@ packages: engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} @@ -1481,6 +1488,12 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + '@willsoto/nestjs-prometheus@6.1.0': + resolution: {integrity: sha512-lrCEnJBBSzUIYWGR+PsZw1YXs1B9jzxFEuNAa3RzTxuFAFdI+sW7Fp52il/U/dX2MWoHc32x06OS0nm56QwyzQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + prom-client: ^15.0.0 + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -1622,6 +1635,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2861,6 +2877,10 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + prom-client@15.1.3: + resolution: {integrity: sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==} + engines: {node: ^16 || ^18 || >=20} + pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -3151,6 +3171,9 @@ packages: resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} engines: {node: '>=6'} + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + terser-webpack-plugin@5.4.0: resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} engines: {node: '>= 10.13.0'} @@ -4231,6 +4254,8 @@ snapshots: dependencies: consola: 3.4.2 + '@opentelemetry/api@1.9.1': {} + '@oxc-project/types@0.124.0': {} '@paralleldrive/cuid2@2.3.1': @@ -4533,7 +4558,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.4': dependencies: @@ -4652,6 +4677,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 + '@willsoto/nestjs-prometheus@6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + prom-client: 15.1.3 + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -4760,6 +4790,8 @@ snapshots: baseline-browser-mapping@2.10.17: {} + bintrees@1.0.2: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -4963,14 +4995,15 @@ snapshots: esbuild: 0.25.12 tsx: 4.21.0 - drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.20.0): + drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0): optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/pg': 8.20.0 pg: 8.20.0 - drizzle-zod@0.8.3(drizzle-orm@0.45.2(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6): + drizzle-zod@0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6): dependencies: - drizzle-orm: 0.45.2(@types/pg@8.20.0)(pg@8.20.0) + drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) zod: 4.3.6 dunder-proto@1.0.1: @@ -5947,6 +5980,11 @@ snapshots: process@0.11.10: {} + prom-client@15.1.3: + dependencies: + '@opentelemetry/api': 1.9.1 + tdigest: 0.1.2 + pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -6254,6 +6292,10 @@ snapshots: tapable@2.3.2: {} + tdigest@0.1.2: + dependencies: + bintrees: 1.0.2 + terser-webpack-plugin@5.4.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack@5.106.0(@swc/core@1.15.24)(esbuild@0.27.7)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -6427,7 +6469,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -6450,6 +6492,7 @@ snapshots: vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 20.19.39 '@vitest/coverage-v8': 4.1.4(vitest@4.1.4) transitivePeerDependencies: From 75960574f305f5d7408a124ff6ad6820ae437595 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:19:23 +0300 Subject: [PATCH 09/47] feat(dump): implement module --- src/app.module.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/app.module.ts b/src/app.module.ts index 0f9f1cf..6af786c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,10 +7,19 @@ import { ConfigService } from '@nestjs/config'; import * as schema from './shared/entities'; import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { ZodValidationPipe, ZodValidationException } from 'nestjs-zod'; +import { PrometheusModule } from '@willsoto/nestjs-prometheus'; @Module({ imports: [ ConfigModule, + PrometheusModule.registerAsync({ + useFactory: () => ({ + path: 'dump', + defaultMetrics: { + enabled: true, + }, + }), + }), DatabaseModule.registerAsync({ global: true, inject: [ConfigService], From f74f19de4c2acea2f300265b590cdebd84540f2c Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:39:15 +0300 Subject: [PATCH 10/47] feat(shared): add health module and integrate into app module --- .../src/controller/health.controller.ts | 39 ++++++++ libs/health/src/controller/health.swagger.ts | 26 +++++ libs/health/src/dtos/health.dto.ts | 25 +++++ libs/health/src/dtos/index.ts | 1 + libs/health/src/health.module.ts | 21 ++++ libs/health/src/health.service.ts | 51 ++++++++++ libs/health/src/index.ts | 1 + libs/health/tsconfig.lib.json | 9 ++ nest-cli.json | 17 +++- src/app.controller.ts | 12 --- src/app.service.ts | 8 -- src/main.ts | 2 +- src/{ => modules/app}/app.controller.spec.ts | 2 - src/modules/app/app.controller.ts | 11 +++ src/{ => modules/app}/app.module.ts | 6 +- test/app.e2e-spec.ts | 2 +- tsconfig.json | 97 ++++++++++++------- 17 files changed, 263 insertions(+), 67 deletions(-) create mode 100644 libs/health/src/controller/health.controller.ts create mode 100644 libs/health/src/controller/health.swagger.ts create mode 100644 libs/health/src/dtos/health.dto.ts create mode 100644 libs/health/src/dtos/index.ts create mode 100644 libs/health/src/health.module.ts create mode 100644 libs/health/src/health.service.ts create mode 100644 libs/health/src/index.ts create mode 100644 libs/health/tsconfig.lib.json delete mode 100644 src/app.controller.ts delete mode 100644 src/app.service.ts rename src/{ => modules/app}/app.controller.spec.ts (87%) create mode 100644 src/modules/app/app.controller.ts rename src/{ => modules/app}/app.module.ts (90%) diff --git a/libs/health/src/controller/health.controller.ts b/libs/health/src/controller/health.controller.ts new file mode 100644 index 0000000..cba9bba --- /dev/null +++ b/libs/health/src/controller/health.controller.ts @@ -0,0 +1,39 @@ +import { Controller, Get, HttpException, HttpStatus, Inject, Logger } from '@nestjs/common'; +import { SkipThrottle } from '@nestjs/throttler'; +import { HealthService } from '../health.service'; +import { GetHealthSwagger, GetPingSwagger } from './health.swagger'; +import { ApiTags } from '@nestjs/swagger'; + +@SkipThrottle() +@Controller() +@ApiTags('System') +export class HealthController { + private logger = new Logger(HealthController.name); + + constructor( + private readonly healthService: HealthService, + @Inject('SERVICE_NAME') private readonly serviceName: string, + ) {} + + @Get('health') + @GetHealthSwagger() + async checkHealth() { + const pingData = await this.healthService.getHealthData(); + + if (pingData.status !== 'up') { + this.logger.error(`${this.serviceName} is unhealthy!`); + throw new HttpException( + `${this.serviceName} service is unhealthy.`, + HttpStatus.SERVICE_UNAVAILABLE, + ); + } + + return 'healthy'; + } + + @Get('ping') + @GetPingSwagger() + async ping() { + return this.healthService.getHealthData(); + } +} diff --git a/libs/health/src/controller/health.swagger.ts b/libs/health/src/controller/health.swagger.ts new file mode 100644 index 0000000..2271969 --- /dev/null +++ b/libs/health/src/controller/health.swagger.ts @@ -0,0 +1,26 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { HealthResponse } from '../dtos'; + +export const GetHealthSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Краткий статус (Health Check)', + description: 'Используется внешними системами для проверки доступности сервиса.', + }), + ApiResponse({ status: 200, description: 'Сервис работает нормально', type: String }), + ApiResponse({ status: 503, description: 'Сервис недоступен или критическая ошибка' }), + ); + +export const GetPingSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Детальный дамп состояния', + description: 'Возвращает аптайм, время старта и метрики памяти.', + }), + ApiResponse({ + status: 200, + description: 'Полная статистика сервиса', + type: HealthResponse.Output, + }), + ); diff --git a/libs/health/src/dtos/health.dto.ts b/libs/health/src/dtos/health.dto.ts new file mode 100644 index 0000000..1877b33 --- /dev/null +++ b/libs/health/src/dtos/health.dto.ts @@ -0,0 +1,25 @@ +import { createZodDto } from 'node_modules/nestjs-zod/dist/dto.cjs'; +import { z } from 'zod/v4'; + +const HealthResponseSchema = z.object({ + service: z.string().describe('Название сервиса'), + status: z.enum(['up', 'down']).describe('Текущий статус'), + info: z.object({ + version: z.string().describe('Версия приложения'), + node: z.string().describe('Версия Node.js'), + pid: z.number().describe('ID процесса'), + }), + time: z.object({ + now: z.string().datetime().describe('Текущее время сервера'), + startedAt: z.string().datetime().describe('Время старта сервера'), + uptime: z.string().describe('Аптайм в формате ч/м/с'), + uptimeSeconds: z.number().describe('Аптайм в секундах'), + }), + metrics: z.object({ + rss: z.string().describe('Resident Set Size (общая память)'), + heapUsed: z.string().describe('Использованная память в куче'), + loadAverage: z.string().describe('Средняя нагрузка на CPU'), + }), +}); + +export class HealthResponse extends createZodDto(HealthResponseSchema) {} diff --git a/libs/health/src/dtos/index.ts b/libs/health/src/dtos/index.ts new file mode 100644 index 0000000..718605a --- /dev/null +++ b/libs/health/src/dtos/index.ts @@ -0,0 +1 @@ +export { HealthResponse } from './health.dto'; diff --git a/libs/health/src/health.module.ts b/libs/health/src/health.module.ts new file mode 100644 index 0000000..c391e72 --- /dev/null +++ b/libs/health/src/health.module.ts @@ -0,0 +1,21 @@ +import { type DynamicModule, Global, Module } from '@nestjs/common'; +import { HealthController } from './controller/health.controller'; +import { HealthService } from './health.service'; + +@Global() +@Module({}) +export class HealthModule { + static register(serviceName: string): DynamicModule { + return { + module: HealthModule, + providers: [ + { + provide: 'SERVICE_NAME', + useValue: serviceName, + }, + HealthService, + ], + controllers: [HealthController], + }; + } +} diff --git a/libs/health/src/health.service.ts b/libs/health/src/health.service.ts new file mode 100644 index 0000000..076a1a6 --- /dev/null +++ b/libs/health/src/health.service.ts @@ -0,0 +1,51 @@ +import { Inject, Injectable } from '@nestjs/common'; +import * as os from 'os'; + +@Injectable() +export class HealthService { + private readonly startTime: Date; + + constructor( + @Inject('SERVICE_NAME') + private readonly serviceName: string, + ) { + this.startTime = new Date(); + } + + async getHealthData() { + const uptimeSeconds = Math.floor(process.uptime()); + const mem = process.memoryUsage(); + + return { + service: this.serviceName, + status: 'up', + info: { + version: '1.0.0', + node: process.version, + pid: process.pid, + }, + time: { + now: new Date().toISOString(), + startedAt: this.startTime.toISOString(), + uptime: this.formatUptime(uptimeSeconds), + uptimeSeconds: uptimeSeconds, + }, + metrics: { + rss: this.toMb(mem.rss), + heapUsed: this.toMb(mem.heapUsed), + loadAverage: os.loadavg()[0].toFixed(2), + }, + }; + } + + private toMb(bytes: number) { + return `${Math.round(bytes / 1024 / 1024)}MB`; + } + + private formatUptime(seconds: number) { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${h}h ${m}m ${s}s`; + } +} diff --git a/libs/health/src/index.ts b/libs/health/src/index.ts new file mode 100644 index 0000000..f0f0421 --- /dev/null +++ b/libs/health/src/index.ts @@ -0,0 +1 @@ +export * from './health.module'; diff --git a/libs/health/tsconfig.lib.json b/libs/health/tsconfig.lib.json new file mode 100644 index 0000000..8e4d095 --- /dev/null +++ b/libs/health/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/health" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json index 8daf27c..5881fec 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -7,6 +7,15 @@ "webpack": true }, "projects": { + "bootstrap": { + "type": "library", + "root": "libs/bootstrap", + "entryFile": "index", + "sourceRoot": "libs/bootstrap/src", + "compilerOptions": { + "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" + } + }, "config": { "type": "library", "root": "libs/config", @@ -25,13 +34,13 @@ "tsConfigPath": "libs/database/tsconfig.lib.json" } }, - "bootstrap": { + "health": { "type": "library", - "root": "libs/bootstrap", + "root": "libs/health", "entryFile": "index", - "sourceRoot": "libs/bootstrap/src", + "sourceRoot": "libs/health/src", "compilerOptions": { - "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" + "tsConfigPath": "libs/health/tsconfig.lib.json" } } } diff --git a/src/app.controller.ts b/src/app.controller.ts deleted file mode 100644 index a325e8b..0000000 --- a/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/src/app.service.ts b/src/app.service.ts deleted file mode 100644 index 61b7a5b..0000000 --- a/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/main.ts b/src/main.ts index aa7c5a0..de5c124 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import { bootstrapApp } from '@libs/bootstrap'; -import { AppModule } from './app.module'; +import { AppModule } from './modules/app/app.module'; bootstrapApp({ serviceName: 'Tracker Monolit', diff --git a/src/app.controller.spec.ts b/src/modules/app/app.controller.spec.ts similarity index 87% rename from src/app.controller.spec.ts rename to src/modules/app/app.controller.spec.ts index 2552ec5..169b786 100644 --- a/src/app.controller.spec.ts +++ b/src/modules/app/app.controller.spec.ts @@ -1,6 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AppController } from './app.controller'; -import { AppService } from './app.service'; describe('AppController', () => { let appController: AppController; @@ -8,7 +7,6 @@ describe('AppController', () => { beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ controllers: [AppController], - providers: [AppService], }).compile(); appController = app.get(AppController); diff --git a/src/modules/app/app.controller.ts b/src/modules/app/app.controller.ts new file mode 100644 index 0000000..eb0cb39 --- /dev/null +++ b/src/modules/app/app.controller.ts @@ -0,0 +1,11 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller() +export class AppController { + constructor() {} + + @Get() + getHello(): string { + return 'Hello World!'; + } +} diff --git a/src/app.module.ts b/src/modules/app/app.module.ts similarity index 90% rename from src/app.module.ts rename to src/modules/app/app.module.ts index 6af786c..c55df26 100644 --- a/src/app.module.ts +++ b/src/modules/app/app.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { ConfigModule } from '@libs/config'; import { DatabaseModule } from '@libs/database'; import { ConfigService } from '@nestjs/config'; -import * as schema from './shared/entities'; +import * as schema from '../../shared/entities'; import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { ZodValidationPipe, ZodValidationException } from 'nestjs-zod'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; +import { HealthModule } from '@libs/health'; @Module({ imports: [ @@ -31,10 +31,10 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus'; }; }, }), + HealthModule.register('gateway'), ], controllers: [AppController], providers: [ - AppService, { provide: APP_PIPE, useClass: ZodValidationPipe, diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 95c5212..0f04656 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import { agent } from 'supertest'; -import { AppModule } from './../src/app.module'; +import { AppModule } from '../src/modules/app/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; diff --git a/tsconfig.json b/tsconfig.json index 503f4c1..759daf0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,38 +1,63 @@ { - "compilerOptions": { - "module": "commonjs", - "declaration": false, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "types": ["node", "vitest/globals"], - "paths": { - "@libs/config": ["./libs/config/src"], - "@libs/config/*": ["./libs/config/src/*"], - "@libs/database": ["./libs/database/src"], - "@libs/database/*": ["./libs/database/src/*"], - "@libs/bootstrap": ["./libs/bootstrap/src"], - "@libs/bootstrap/*": ["./libs/bootstrap/src/*"] - } - }, - "include": [ - "src/**/*", - "libs/**/*", - "test/**/*", - "drizzle.config.ts", - "vitest.config.ts", - "vitest.config.e2e.ts" + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "types": [ + "node", + "vitest/globals" ], - "exclude": ["dist", "node_modules"] -} + "paths": { + "@libs/bootstrap": [ + "./libs/bootstrap/src" + ], + "@libs/bootstrap/*": [ + "./libs/bootstrap/src/*" + ], + "@libs/config": [ + "./libs/config/src" + ], + "@libs/config/*": [ + "./libs/config/src/*" + ], + "@libs/database": [ + "./libs/database/src" + ], + "@libs/database/*": [ + "./libs/database/src/*" + ], + "@libs/health": [ + "libs/health/src" + ], + "@libs/health/*": [ + "libs/health/src/*" + ] + }, + "baseUrl": "./" + }, + "include": [ + "src/**/*", + "libs/**/*", + "test/**/*", + "drizzle.config.ts", + "vitest.config.ts", + "vitest.config.e2e.ts" + ], + "exclude": [ + "dist", + "node_modules" + ] +} \ No newline at end of file From 23cf64a8549c32f82aa74af9ac3183fd0180e9b9 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:47:14 +0300 Subject: [PATCH 11/47] resolve: resolve merge error --- pnpm-lock.yaml | 466 +------------------------------------------------ 1 file changed, 5 insertions(+), 461 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a06f69f..f9fac8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,12 +66,6 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: - '@commitlint/cli': - specifier: ^20.5.0 - version: 20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3) - '@commitlint/config-conventional': - specifier: ^20.5.0 - version: 20.5.0 '@nestjs/cli': specifier: ^11.0.19 version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) @@ -111,9 +105,6 @@ importers: eslint-plugin-prettier: specifier: ^5.0.0 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2) - husky: - specifier: ^9.1.7 - version: 9.1.7 lint-staged: specifier: ^16.4.0 version: 16.4.0 @@ -143,7 +134,7 @@ importers: version: 1.5.9(@swc/core@1.15.24) vitest: specifier: ^4.1.4 - version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -210,87 +201,6 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@commitlint/cli@20.5.0': - resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} - engines: {node: '>=v18'} - hasBin: true - - '@commitlint/config-conventional@20.5.0': - resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} - engines: {node: '>=v18'} - - '@commitlint/config-validator@20.5.0': - resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} - engines: {node: '>=v18'} - - '@commitlint/ensure@20.5.0': - resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} - engines: {node: '>=v18'} - - '@commitlint/execute-rule@20.0.0': - resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} - engines: {node: '>=v18'} - - '@commitlint/format@20.5.0': - resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} - engines: {node: '>=v18'} - - '@commitlint/is-ignored@20.5.0': - resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} - engines: {node: '>=v18'} - - '@commitlint/lint@20.5.0': - resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} - engines: {node: '>=v18'} - - '@commitlint/load@20.5.0': - resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} - engines: {node: '>=v18'} - - '@commitlint/message@20.4.3': - resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} - engines: {node: '>=v18'} - - '@commitlint/parse@20.5.0': - resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} - engines: {node: '>=v18'} - - '@commitlint/read@20.5.0': - resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} - engines: {node: '>=v18'} - - '@commitlint/resolve-extends@20.5.0': - resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} - engines: {node: '>=v18'} - - '@commitlint/rules@20.5.0': - resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} - engines: {node: '>=v18'} - - '@commitlint/to-lines@20.0.0': - resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} - engines: {node: '>=v18'} - - '@commitlint/top-level@20.4.3': - resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} - engines: {node: '>=v18'} - - '@commitlint/types@20.5.0': - resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} - engines: {node: '>=v18'} - - '@conventional-changelog/git-client@2.7.0': - resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==} - engines: {node: '>=18'} - peerDependencies: - conventional-commits-filter: ^5.0.0 - conventional-commits-parser: ^6.4.0 - peerDependenciesMeta: - conventional-commits-filter: - optional: true - conventional-commits-parser: - optional: true - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1277,14 +1187,6 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@simple-libs/child-process-utils@1.0.2': - resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} - engines: {node: '>=18'} - - '@simple-libs/stream-utils@1.2.0': - resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} - engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1691,9 +1593,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1828,10 +1727,6 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1865,9 +1760,6 @@ packages: resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} - compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1882,19 +1774,6 @@ packages: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} - conventional-changelog-angular@8.3.1: - resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} - engines: {node: '>=18'} - - conventional-changelog-conventionalcommits@9.3.1: - resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} - engines: {node: '>=18'} - - conventional-commits-parser@6.4.0: - resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} - engines: {node: '>=18'} - hasBin: true - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1908,14 +1787,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig-typescript-loader@6.3.0: - resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} - engines: {node: '>=v18'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=9' - typescript: '>=5' - cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -1925,15 +1796,6 @@ packages: typescript: optional: true - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -1991,10 +1853,6 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} @@ -2135,10 +1993,6 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2387,10 +2241,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -2406,11 +2256,6 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - git-raw-commits@5.0.1: - resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} - engines: {node: '>=18'} - hasBin: true - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2430,10 +2275,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} - globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -2475,11 +2316,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2495,9 +2331,6 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-meta-resolve@4.2.0: - resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2509,10 +2342,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2544,18 +2373,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2741,27 +2562,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2801,10 +2604,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - meow@13.2.0: - resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3128,10 +2927,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3140,10 +2935,6 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3674,10 +3465,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3689,10 +3476,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -3702,10 +3485,6 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3803,128 +3582,6 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': - dependencies: - '@commitlint/format': 20.5.0 - '@commitlint/lint': 20.5.0 - '@commitlint/load': 20.5.0(@types/node@20.19.39)(typescript@5.9.3) - '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) - '@commitlint/types': 20.5.0 - tinyexec: 1.1.1 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - conventional-commits-filter - - conventional-commits-parser - - typescript - - '@commitlint/config-conventional@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - conventional-changelog-conventionalcommits: 9.3.1 - - '@commitlint/config-validator@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - ajv: 8.18.0 - - '@commitlint/ensure@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.startcase: 4.4.0 - lodash.upperfirst: 4.3.1 - - '@commitlint/execute-rule@20.0.0': {} - - '@commitlint/format@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - picocolors: 1.1.1 - - '@commitlint/is-ignored@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - semver: 7.7.4 - - '@commitlint/lint@20.5.0': - dependencies: - '@commitlint/is-ignored': 20.5.0 - '@commitlint/parse': 20.5.0 - '@commitlint/rules': 20.5.0 - '@commitlint/types': 20.5.0 - - '@commitlint/load@20.5.0(@types/node@20.19.39)(typescript@5.9.3)': - dependencies: - '@commitlint/config-validator': 20.5.0 - '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.5.0 - '@commitlint/types': 20.5.0 - cosmiconfig: 9.0.1(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) - is-plain-obj: 4.1.0 - lodash.mergewith: 4.6.2 - picocolors: 1.1.1 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/message@20.4.3': {} - - '@commitlint/parse@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - conventional-changelog-angular: 8.3.1 - conventional-commits-parser: 6.4.0 - - '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)': - dependencies: - '@commitlint/top-level': 20.4.3 - '@commitlint/types': 20.5.0 - git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) - minimist: 1.2.8 - tinyexec: 1.1.1 - transitivePeerDependencies: - - conventional-commits-filter - - conventional-commits-parser - - '@commitlint/resolve-extends@20.5.0': - dependencies: - '@commitlint/config-validator': 20.5.0 - '@commitlint/types': 20.5.0 - global-directory: 4.0.1 - import-meta-resolve: 4.2.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - - '@commitlint/rules@20.5.0': - dependencies: - '@commitlint/ensure': 20.5.0 - '@commitlint/message': 20.4.3 - '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.5.0 - - '@commitlint/to-lines@20.0.0': {} - - '@commitlint/top-level@20.4.3': - dependencies: - escalade: 3.2.0 - - '@commitlint/types@20.5.0': - dependencies: - conventional-commits-parser: 6.4.0 - picocolors: 1.1.1 - - '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)': - dependencies: - '@simple-libs/child-process-utils': 1.0.2 - '@simple-libs/stream-utils': 1.2.0 - semver: 7.7.4 - optionalDependencies: - conventional-commits-parser: 6.4.0 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4672,12 +4329,6 @@ snapshots: '@scarf/scarf@1.4.0': {} - '@simple-libs/child-process-utils@1.0.2': - dependencies: - '@simple-libs/stream-utils': 1.2.0 - - '@simple-libs/stream-utils@1.2.0': {} - '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -4911,7 +4562,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.4': dependencies: @@ -5112,8 +4763,6 @@ snapshots: argparse@2.0.1: {} - array-ify@1.0.0: {} - array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -5242,12 +4891,6 @@ snapshots: cli-width@4.1.0: {} - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clone@1.0.4: {} color-convert@2.0.1: @@ -5273,11 +4916,6 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 - compare-func@2.0.0: - dependencies: - array-ify: 1.0.0 - dot-prop: 5.3.0 - component-emitter@1.3.1: {} concat-map@0.0.1: {} @@ -5286,19 +4924,6 @@ snapshots: content-disposition@1.1.0: {} - conventional-changelog-angular@8.3.1: - dependencies: - compare-func: 2.0.0 - - conventional-changelog-conventionalcommits@9.3.1: - dependencies: - compare-func: 2.0.0 - - conventional-commits-parser@6.4.0: - dependencies: - '@simple-libs/stream-utils': 1.2.0 - meow: 13.2.0 - convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -5307,13 +4932,6 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): - dependencies: - '@types/node': 20.19.39 - cosmiconfig: 9.0.1(typescript@5.9.3) - jiti: 2.6.1 - typescript: 5.9.3 - cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -5323,15 +4941,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - cosmiconfig@9.0.1(typescript@5.9.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.3 - create-require@1.1.1: {} cross-spawn@7.0.6: @@ -5375,10 +4984,6 @@ snapshots: dependencies: esutils: 2.0.3 - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - dotenv-expand@12.0.3: dependencies: dotenv: 16.6.1 @@ -5440,8 +5045,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 - env-paths@2.2.1: {} - environment@1.1.0: {} error-ex@1.3.4: @@ -5805,8 +5408,6 @@ snapshots: function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -5831,14 +5432,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - git-raw-commits@5.0.1(conventional-commits-parser@6.4.0): - dependencies: - '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0) - meow: 13.2.0 - transitivePeerDependencies: - - conventional-commits-filter - - conventional-commits-parser - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5864,10 +5457,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global-directory@4.0.1: - dependencies: - ini: 4.1.1 - globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -5909,8 +5498,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - husky@9.1.7: {} - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5924,8 +5511,6 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-meta-resolve@4.2.0: {} - imurmurhash@0.1.4: {} inflight@1.0.6: @@ -5935,8 +5520,6 @@ snapshots: inherits@2.0.4: {} - ini@4.1.1: {} - ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -5957,12 +5540,8 @@ snapshots: is-number@7.0.0: {} - is-obj@2.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} - is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -5990,7 +5569,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-tokens@10.0.0: {} @@ -6118,20 +5698,8 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} - - lodash.snakecase@4.1.1: {} - - lodash.startcase@4.4.0: {} - - lodash.upperfirst@4.3.1: {} - lodash@4.18.1: {} log-symbols@4.1.0: @@ -6175,8 +5743,6 @@ snapshots: dependencies: fs-monkey: 1.1.0 - meow@13.2.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6477,14 +6043,10 @@ snapshots: reflect-metadata@0.2.2: {} - require-directory@2.1.1: {} - require-from-string@2.0.2: {} resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} restore-cursor@3.1.0: @@ -6915,7 +6477,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -7008,12 +6570,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -7024,22 +6580,10 @@ snapshots: xtend@4.0.2: {} - y18n@5.0.8: {} - yaml@2.8.3: {} yargs-parser@21.1.1: {} - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yn@3.1.1: {} yocto-queue@0.1.0: {} From 9ba930dd0c8770400e087903b812d79361e7aacd Mon Sep 17 00:00:00 2001 From: Maksym Berehovyi <108676512+maksberegovoi@users.noreply.github.com> Date: Fri, 10 Apr 2026 23:40:31 +0300 Subject: [PATCH 12/47] chore: setup husky, commitlint and lint-staged --- .commitlintrc.mjs | 3 + .husky/commit-msg | 1 + .lintstagedrc.mjs | 2 +- CONTRIBUTING.md | 1 + package.json | 145 +++++++------- pnpm-lock.yaml | 480 +++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 552 insertions(+), 80 deletions(-) create mode 100644 .commitlintrc.mjs create mode 100755 .husky/commit-msg diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs new file mode 100644 index 0000000..803dc4f --- /dev/null +++ b/.commitlintrc.mjs @@ -0,0 +1,3 @@ +export default { + extends: ['@commitlint/config-conventional'], +}; diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..0a4b97d --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit $1 diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index 68caded..90da1d6 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,4 +1,4 @@ export default { - '*.{ts,js}': ['eslint --fix', 'prettier --write'], + '*.{ts,js}': ['eslint --fix'], '*.{json,css,md}': ['prettier --write'], }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8272506..6aa8cca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ - `feat/` — для новых функциональных возможностей. - `fix/` — для исправления багов. - `refactor/` — для переписывания кода без изменения логики. +- `chore/` — для технических задач, настройки окружения и зависимостей - `docs/` — для обновления документации. ## 2. Commit Message Convention diff --git a/package.json b/package.json index e6212c8..e770cf6 100644 --- a/package.json +++ b/package.json @@ -1,72 +1,75 @@ { - "name": "task-backend", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "vitest run", - "test:watch": "vitest", - "test:cov": "vitest run --coverage", - "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false", - "test:e2e": "vitest run --config ./vitest.config.e2e.ts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@fastify/compress": "^8.3.1", - "@fastify/cookie": "^11.0.2", - "@fastify/cors": "^11.2.0", - "@fastify/static": "^9.1.0", - "@nestjs/common": "^11.1.18", - "@nestjs/config": "^4.0.4", - "@nestjs/core": "^11.1.18", - "@nestjs/platform-fastify": "^11.1.18", - "@nestjs/swagger": "^11.2.7", - "@nestjs/throttler": "^6.5.0", - "@willsoto/nestjs-prometheus": "^6.1.0", - "drizzle-orm": "^0.45.2", - "drizzle-zod": "^0.8.3", - "fastify": "^5.8.4", - "nestjs-zod": "^5.3.0", - "pg": "^8.20.0", - "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1", - "zod": "^4.3.6" - }, - "devDependencies": { - "@nestjs/cli": "^11.0.19", - "@nestjs/schematics": "^11.0.10", - "@nestjs/testing": "^11.1.18", - "@types/node": "^20.3.1", - "@types/pg": "^8.20.0", - "@types/supertest": "^6.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitest/coverage-v8": "^4.1.4", - "drizzle-kit": "^0.31.10", - "eslint": "^8.42.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "lint-staged": "^16.4.0", - "prettier": "^3.0.0", - "source-map-support": "^0.5.21", - "supertest": "^6.3.3", - "ts-loader": "^9.4.3", - "ts-node": "^10.9.1", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.1.3", - "unplugin-swc": "^1.5.9", - "vitest": "^4.1.4" - }, - "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" -} \ No newline at end of file + "name": "task-backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "vitest run", + "test:watch": "vitest", + "test:cov": "vitest run --coverage", + "test:debug": "vitest --inspect-brk --inspect --logHeapUsage --threads=false", + "test:e2e": "vitest run --config ./vitest.config.e2e.ts", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "prepare": "husky" + }, + "dependencies": { + "@fastify/compress": "^8.3.1", + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^11.2.0", + "@fastify/static": "^9.1.0", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/platform-fastify": "^11.1.18", + "@nestjs/swagger": "^11.2.7", + "@nestjs/throttler": "^6.5.0", + "drizzle-orm": "^0.45.2", + "drizzle-zod": "^0.8.3", + "fastify": "^5.8.4", + "nestjs-zod": "^5.3.0", + "pg": "^8.20.0", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@commitlint/cli": "^20.5.0", + "@commitlint/config-conventional": "^20.5.0", + "@nestjs/cli": "^11.0.19", + "@nestjs/schematics": "^11.0.10", + "@nestjs/testing": "^11.1.18", + "@types/node": "^20.3.1", + "@types/pg": "^8.20.0", + "@types/supertest": "^6.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "@vitest/coverage-v8": "^4.1.4", + "drizzle-kit": "^0.31.10", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "husky": "^9.1.7", + "lint-staged": "^16.4.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3", + "unplugin-swc": "^1.5.9", + "vitest": "^4.1.4" + }, + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b41984..19a16fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,12 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@commitlint/cli': + specifier: ^20.5.0 + version: 20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^20.5.0 + version: 20.5.0 '@nestjs/cli': specifier: ^11.0.19 version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) @@ -105,6 +111,9 @@ importers: eslint-plugin-prettier: specifier: ^5.0.0 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2) + husky: + specifier: ^9.1.7 + version: 9.1.7 lint-staged: specifier: ^16.4.0 version: 16.4.0 @@ -134,7 +143,7 @@ importers: version: 1.5.9(@swc/core@1.15.24) vitest: specifier: ^4.1.4 - version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -201,6 +210,87 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@commitlint/cli@20.5.0': + resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@20.5.0': + resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.5.0': + resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@20.5.0': + resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@20.5.0': + resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@20.5.0': + resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} + engines: {node: '>=v18'} + + '@commitlint/lint@20.5.0': + resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} + engines: {node: '>=v18'} + + '@commitlint/load@20.5.0': + resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} + engines: {node: '>=v18'} + + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@20.5.0': + resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} + engines: {node: '>=v18'} + + '@commitlint/read@20.5.0': + resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.5.0': + resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} + engines: {node: '>=v18'} + + '@commitlint/rules@20.5.0': + resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@20.0.0': + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} + + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} + engines: {node: '>=v18'} + + '@commitlint/types@20.5.0': + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} + engines: {node: '>=v18'} + + '@conventional-changelog/git-client@2.7.0': + resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==} + engines: {node: '>=18'} + peerDependencies: + conventional-commits-filter: ^5.0.0 + conventional-commits-parser: ^6.4.0 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1187,6 +1277,14 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@simple-libs/child-process-utils@1.0.2': + resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} + engines: {node: '>=18'} + + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1593,6 +1691,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1727,6 +1828,10 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1760,6 +1865,9 @@ packages: resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1774,6 +1882,19 @@ packages: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} + conventional-changelog-angular@8.3.1: + resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} + engines: {node: '>=18'} + + conventional-changelog-conventionalcommits@9.3.1: + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} + engines: {node: '>=18'} + + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1787,6 +1908,14 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -1796,6 +1925,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -1853,6 +1991,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} @@ -1993,6 +2135,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2241,6 +2387,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -2256,6 +2406,11 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + git-raw-commits@5.0.1: + resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} + engines: {node: '>=18'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2275,6 +2430,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -2316,6 +2475,11 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2331,6 +2495,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2342,6 +2509,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2373,10 +2544,18 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2407,6 +2586,10 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -2558,9 +2741,27 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2600,6 +2801,10 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2923,6 +3128,10 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2931,6 +3140,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3461,6 +3674,10 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3472,6 +3689,10 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -3481,6 +3702,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3578,6 +3803,128 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 20.5.0 + '@commitlint/lint': 20.5.0 + '@commitlint/load': 20.5.0(@types/node@20.19.39)(typescript@5.9.3) + '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) + '@commitlint/types': 20.5.0 + tinyexec: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-conventionalcommits: 9.3.1 + + '@commitlint/config-validator@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + ajv: 8.18.0 + + '@commitlint/ensure@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@20.0.0': {} + + '@commitlint/format@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + semver: 7.7.4 + + '@commitlint/lint@20.5.0': + dependencies: + '@commitlint/is-ignored': 20.5.0 + '@commitlint/parse': 20.5.0 + '@commitlint/rules': 20.5.0 + '@commitlint/types': 20.5.0 + + '@commitlint/load@20.5.0(@types/node@20.19.39)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.5.0 + '@commitlint/types': 20.5.0 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) + is-plain-obj: 4.1.0 + lodash.mergewith: 4.6.2 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@20.4.3': {} + + '@commitlint/parse@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-angular: 8.3.1 + conventional-commits-parser: 6.4.0 + + '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)': + dependencies: + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.5.0 + git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) + minimist: 1.2.8 + tinyexec: 1.1.1 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@20.5.0': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/types': 20.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@20.5.0': + dependencies: + '@commitlint/ensure': 20.5.0 + '@commitlint/message': 20.4.3 + '@commitlint/to-lines': 20.0.0 + '@commitlint/types': 20.5.0 + + '@commitlint/to-lines@20.0.0': {} + + '@commitlint/top-level@20.4.3': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@20.5.0': + dependencies: + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)': + dependencies: + '@simple-libs/child-process-utils': 1.0.2 + '@simple-libs/stream-utils': 1.2.0 + semver: 7.7.4 + optionalDependencies: + conventional-commits-parser: 6.4.0 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4325,6 +4672,12 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@simple-libs/child-process-utils@1.0.2': + dependencies: + '@simple-libs/stream-utils': 1.2.0 + + '@simple-libs/stream-utils@1.2.0': {} + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -4558,7 +4911,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.4': dependencies: @@ -4569,13 +4922,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@4.1.4': dependencies: @@ -4759,6 +5112,8 @@ snapshots: argparse@2.0.1: {} + array-ify@1.0.0: {} + array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -4887,6 +5242,12 @@ snapshots: cli-width@4.1.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@2.0.1: @@ -4912,6 +5273,11 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + component-emitter@1.3.1: {} concat-map@0.0.1: {} @@ -4920,6 +5286,19 @@ snapshots: content-disposition@1.1.0: {} + conventional-changelog-angular@8.3.1: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@9.3.1: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -4928,6 +5307,13 @@ snapshots: core-util-is@1.0.3: {} + cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 20.19.39 + cosmiconfig: 9.0.1(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -4937,6 +5323,15 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + create-require@1.1.1: {} cross-spawn@7.0.6: @@ -4980,6 +5375,10 @@ snapshots: dependencies: esutils: 2.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + dotenv-expand@12.0.3: dependencies: dotenv: 16.6.1 @@ -5041,6 +5440,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + env-paths@2.2.1: {} + environment@1.1.0: {} error-ex@1.3.4: @@ -5404,6 +5805,8 @@ snapshots: function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -5428,6 +5831,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + git-raw-commits@5.0.1(conventional-commits-parser@6.4.0): + dependencies: + '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0) + meow: 13.2.0 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5453,6 +5864,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -5494,6 +5909,8 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + husky@9.1.7: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5507,6 +5924,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} inflight@1.0.6: @@ -5516,6 +5935,8 @@ snapshots: inherits@2.0.4: {} + ini@4.1.1: {} + ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -5536,8 +5957,12 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -5565,6 +5990,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jiti@2.6.1: {} + js-tokens@10.0.0: {} js-tokens@4.0.0: {} @@ -5691,8 +6118,20 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + + lodash.kebabcase@4.1.1: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.upperfirst@4.3.1: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -5736,6 +6175,8 @@ snapshots: dependencies: fs-monkey: 1.1.0 + meow@13.2.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6036,10 +6477,14 @@ snapshots: reflect-metadata@0.2.2: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} restore-cursor@3.1.0: @@ -6454,7 +6899,7 @@ snapshots: v8-compile-cache-lib@3.0.1: {} - vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -6465,14 +6910,15 @@ snapshots: '@types/node': 20.19.39 esbuild: 0.27.7 fsevents: 2.3.3 + jiti: 2.6.1 terser: 5.46.1 tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.4 '@vitest/runner': 4.1.4 '@vitest/snapshot': 4.1.4 @@ -6489,7 +6935,7 @@ snapshots: tinyexec: 1.1.1 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -6562,6 +7008,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -6572,10 +7024,22 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yaml@2.8.3: {} yargs-parser@21.1.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yn@3.1.1: {} yocto-queue@0.1.0: {} From 9033200d2b1de2a252352c9ba995ab36626b3abf Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:47:14 +0300 Subject: [PATCH 13/47] resolve: resolve merge error --- pnpm-lock.yaml | 466 +------------------------------------------------ 1 file changed, 5 insertions(+), 461 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19a16fe..efb5f61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,12 +66,6 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: - '@commitlint/cli': - specifier: ^20.5.0 - version: 20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3) - '@commitlint/config-conventional': - specifier: ^20.5.0 - version: 20.5.0 '@nestjs/cli': specifier: ^11.0.19 version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) @@ -111,9 +105,6 @@ importers: eslint-plugin-prettier: specifier: ^5.0.0 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2) - husky: - specifier: ^9.1.7 - version: 9.1.7 lint-staged: specifier: ^16.4.0 version: 16.4.0 @@ -143,7 +134,7 @@ importers: version: 1.5.9(@swc/core@1.15.24) vitest: specifier: ^4.1.4 - version: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages: @@ -210,87 +201,6 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@commitlint/cli@20.5.0': - resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} - engines: {node: '>=v18'} - hasBin: true - - '@commitlint/config-conventional@20.5.0': - resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} - engines: {node: '>=v18'} - - '@commitlint/config-validator@20.5.0': - resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} - engines: {node: '>=v18'} - - '@commitlint/ensure@20.5.0': - resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} - engines: {node: '>=v18'} - - '@commitlint/execute-rule@20.0.0': - resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} - engines: {node: '>=v18'} - - '@commitlint/format@20.5.0': - resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} - engines: {node: '>=v18'} - - '@commitlint/is-ignored@20.5.0': - resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} - engines: {node: '>=v18'} - - '@commitlint/lint@20.5.0': - resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} - engines: {node: '>=v18'} - - '@commitlint/load@20.5.0': - resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} - engines: {node: '>=v18'} - - '@commitlint/message@20.4.3': - resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} - engines: {node: '>=v18'} - - '@commitlint/parse@20.5.0': - resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} - engines: {node: '>=v18'} - - '@commitlint/read@20.5.0': - resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} - engines: {node: '>=v18'} - - '@commitlint/resolve-extends@20.5.0': - resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} - engines: {node: '>=v18'} - - '@commitlint/rules@20.5.0': - resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} - engines: {node: '>=v18'} - - '@commitlint/to-lines@20.0.0': - resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} - engines: {node: '>=v18'} - - '@commitlint/top-level@20.4.3': - resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} - engines: {node: '>=v18'} - - '@commitlint/types@20.5.0': - resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} - engines: {node: '>=v18'} - - '@conventional-changelog/git-client@2.7.0': - resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==} - engines: {node: '>=18'} - peerDependencies: - conventional-commits-filter: ^5.0.0 - conventional-commits-parser: ^6.4.0 - peerDependenciesMeta: - conventional-commits-filter: - optional: true - conventional-commits-parser: - optional: true - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1277,14 +1187,6 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} - '@simple-libs/child-process-utils@1.0.2': - resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} - engines: {node: '>=18'} - - '@simple-libs/stream-utils@1.2.0': - resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} - engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1691,9 +1593,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1828,10 +1727,6 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1865,9 +1760,6 @@ packages: resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} - compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} - component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1882,19 +1774,6 @@ packages: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} - conventional-changelog-angular@8.3.1: - resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} - engines: {node: '>=18'} - - conventional-changelog-conventionalcommits@9.3.1: - resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} - engines: {node: '>=18'} - - conventional-commits-parser@6.4.0: - resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} - engines: {node: '>=18'} - hasBin: true - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1908,14 +1787,6 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig-typescript-loader@6.3.0: - resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} - engines: {node: '>=v18'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=9' - typescript: '>=5' - cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -1925,15 +1796,6 @@ packages: typescript: optional: true - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -1991,10 +1853,6 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} @@ -2135,10 +1993,6 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2387,10 +2241,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -2406,11 +2256,6 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - git-raw-commits@5.0.1: - resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} - engines: {node: '>=18'} - hasBin: true - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2430,10 +2275,6 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-directory@4.0.1: - resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} - engines: {node: '>=18'} - globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -2475,11 +2316,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2495,9 +2331,6 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-meta-resolve@4.2.0: - resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2509,10 +2342,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@4.1.1: - resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2544,18 +2373,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2741,27 +2562,9 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - - lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - - lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2801,10 +2604,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - meow@13.2.0: - resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} - engines: {node: '>=18'} - merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3128,10 +2927,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3140,10 +2935,6 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3674,10 +3465,6 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3689,10 +3476,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -3702,10 +3485,6 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3803,128 +3582,6 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': - dependencies: - '@commitlint/format': 20.5.0 - '@commitlint/lint': 20.5.0 - '@commitlint/load': 20.5.0(@types/node@20.19.39)(typescript@5.9.3) - '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) - '@commitlint/types': 20.5.0 - tinyexec: 1.1.1 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - conventional-commits-filter - - conventional-commits-parser - - typescript - - '@commitlint/config-conventional@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - conventional-changelog-conventionalcommits: 9.3.1 - - '@commitlint/config-validator@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - ajv: 8.18.0 - - '@commitlint/ensure@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - lodash.camelcase: 4.3.0 - lodash.kebabcase: 4.1.1 - lodash.snakecase: 4.1.1 - lodash.startcase: 4.4.0 - lodash.upperfirst: 4.3.1 - - '@commitlint/execute-rule@20.0.0': {} - - '@commitlint/format@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - picocolors: 1.1.1 - - '@commitlint/is-ignored@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - semver: 7.7.4 - - '@commitlint/lint@20.5.0': - dependencies: - '@commitlint/is-ignored': 20.5.0 - '@commitlint/parse': 20.5.0 - '@commitlint/rules': 20.5.0 - '@commitlint/types': 20.5.0 - - '@commitlint/load@20.5.0(@types/node@20.19.39)(typescript@5.9.3)': - dependencies: - '@commitlint/config-validator': 20.5.0 - '@commitlint/execute-rule': 20.0.0 - '@commitlint/resolve-extends': 20.5.0 - '@commitlint/types': 20.5.0 - cosmiconfig: 9.0.1(typescript@5.9.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) - is-plain-obj: 4.1.0 - lodash.mergewith: 4.6.2 - picocolors: 1.1.1 - transitivePeerDependencies: - - '@types/node' - - typescript - - '@commitlint/message@20.4.3': {} - - '@commitlint/parse@20.5.0': - dependencies: - '@commitlint/types': 20.5.0 - conventional-changelog-angular: 8.3.1 - conventional-commits-parser: 6.4.0 - - '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)': - dependencies: - '@commitlint/top-level': 20.4.3 - '@commitlint/types': 20.5.0 - git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) - minimist: 1.2.8 - tinyexec: 1.1.1 - transitivePeerDependencies: - - conventional-commits-filter - - conventional-commits-parser - - '@commitlint/resolve-extends@20.5.0': - dependencies: - '@commitlint/config-validator': 20.5.0 - '@commitlint/types': 20.5.0 - global-directory: 4.0.1 - import-meta-resolve: 4.2.0 - lodash.mergewith: 4.6.2 - resolve-from: 5.0.0 - - '@commitlint/rules@20.5.0': - dependencies: - '@commitlint/ensure': 20.5.0 - '@commitlint/message': 20.4.3 - '@commitlint/to-lines': 20.0.0 - '@commitlint/types': 20.5.0 - - '@commitlint/to-lines@20.0.0': {} - - '@commitlint/top-level@20.4.3': - dependencies: - escalade: 3.2.0 - - '@commitlint/types@20.5.0': - dependencies: - conventional-commits-parser: 6.4.0 - picocolors: 1.1.1 - - '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)': - dependencies: - '@simple-libs/child-process-utils': 1.0.2 - '@simple-libs/stream-utils': 1.2.0 - semver: 7.7.4 - optionalDependencies: - conventional-commits-parser: 6.4.0 - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4672,12 +4329,6 @@ snapshots: '@scarf/scarf@1.4.0': {} - '@simple-libs/child-process-utils@1.0.2': - dependencies: - '@simple-libs/stream-utils': 1.2.0 - - '@simple-libs/stream-utils@1.2.0': {} - '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -4911,7 +4562,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.4': dependencies: @@ -5112,8 +4763,6 @@ snapshots: argparse@2.0.1: {} - array-ify@1.0.0: {} - array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -5242,12 +4891,6 @@ snapshots: cli-width@4.1.0: {} - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - clone@1.0.4: {} color-convert@2.0.1: @@ -5273,11 +4916,6 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 - compare-func@2.0.0: - dependencies: - array-ify: 1.0.0 - dot-prop: 5.3.0 - component-emitter@1.3.1: {} concat-map@0.0.1: {} @@ -5286,19 +4924,6 @@ snapshots: content-disposition@1.1.0: {} - conventional-changelog-angular@8.3.1: - dependencies: - compare-func: 2.0.0 - - conventional-changelog-conventionalcommits@9.3.1: - dependencies: - compare-func: 2.0.0 - - conventional-commits-parser@6.4.0: - dependencies: - '@simple-libs/stream-utils': 1.2.0 - meow: 13.2.0 - convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -5307,13 +4932,6 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): - dependencies: - '@types/node': 20.19.39 - cosmiconfig: 9.0.1(typescript@5.9.3) - jiti: 2.6.1 - typescript: 5.9.3 - cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -5323,15 +4941,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - cosmiconfig@9.0.1(typescript@5.9.3): - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optionalDependencies: - typescript: 5.9.3 - create-require@1.1.1: {} cross-spawn@7.0.6: @@ -5375,10 +4984,6 @@ snapshots: dependencies: esutils: 2.0.3 - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - dotenv-expand@12.0.3: dependencies: dotenv: 16.6.1 @@ -5440,8 +5045,6 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 - env-paths@2.2.1: {} - environment@1.1.0: {} error-ex@1.3.4: @@ -5805,8 +5408,6 @@ snapshots: function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -5831,14 +5432,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - git-raw-commits@5.0.1(conventional-commits-parser@6.4.0): - dependencies: - '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0) - meow: 13.2.0 - transitivePeerDependencies: - - conventional-commits-filter - - conventional-commits-parser - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5864,10 +5457,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global-directory@4.0.1: - dependencies: - ini: 4.1.1 - globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -5909,8 +5498,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - husky@9.1.7: {} - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5924,8 +5511,6 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-meta-resolve@4.2.0: {} - imurmurhash@0.1.4: {} inflight@1.0.6: @@ -5935,8 +5520,6 @@ snapshots: inherits@2.0.4: {} - ini@4.1.1: {} - ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -5957,12 +5540,8 @@ snapshots: is-number@7.0.0: {} - is-obj@2.0.0: {} - is-path-inside@3.0.3: {} - is-plain-obj@4.1.0: {} - is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -5990,7 +5569,8 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@2.6.1: {} + jiti@2.6.1: + optional: true js-tokens@10.0.0: {} @@ -6118,20 +5698,8 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash.camelcase@4.3.0: {} - - lodash.kebabcase@4.1.1: {} - lodash.merge@4.6.2: {} - lodash.mergewith@4.6.2: {} - - lodash.snakecase@4.1.1: {} - - lodash.startcase@4.4.0: {} - - lodash.upperfirst@4.3.1: {} - lodash@4.18.1: {} log-symbols@4.1.0: @@ -6175,8 +5743,6 @@ snapshots: dependencies: fs-monkey: 1.1.0 - meow@13.2.0: {} - merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6477,14 +6043,10 @@ snapshots: reflect-metadata@0.2.2: {} - require-directory@2.1.1: {} - require-from-string@2.0.2: {} resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} restore-cursor@3.1.0: @@ -6915,7 +6477,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@4.1.4(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vitest@4.1.4(@opentelemetry/api@1.9.1)(@types/node@20.19.39)(@vitest/coverage-v8@4.1.4)(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) @@ -7008,12 +6570,6 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -7024,22 +6580,10 @@ snapshots: xtend@4.0.2: {} - y18n@5.0.8: {} - yaml@2.8.3: {} yargs-parser@21.1.1: {} - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yn@3.1.1: {} yocto-queue@0.1.0: {} From 62b8b1507f0e90d17548f78ed2fa63d7927b4312 Mon Sep 17 00:00:00 2001 From: soorq Date: Fri, 10 Apr 2026 23:51:58 +0300 Subject: [PATCH 14/47] chore: resolve error with merges --- package.json | 1 + pnpm-lock.yaml | 460 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 459 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e770cf6..3aee9e1 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/platform-fastify": "^11.1.18", "@nestjs/swagger": "^11.2.7", "@nestjs/throttler": "^6.5.0", + "@willsoto/nestjs-prometheus": "^6.1.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "fastify": "^5.8.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9fac8a..91a0420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,6 +66,12 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@commitlint/cli': + specifier: ^20.5.0 + version: 20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: ^20.5.0 + version: 20.5.0 '@nestjs/cli': specifier: ^11.0.19 version: 11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7) @@ -105,6 +111,9 @@ importers: eslint-plugin-prettier: specifier: ^5.0.0 version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@9.1.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.2) + husky: + specifier: ^9.1.7 + version: 9.1.7 lint-staged: specifier: ^16.4.0 version: 16.4.0 @@ -201,6 +210,87 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@commitlint/cli@20.5.0': + resolution: {integrity: sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@20.5.0': + resolution: {integrity: sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@20.5.0': + resolution: {integrity: sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@20.5.0': + resolution: {integrity: sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@20.0.0': + resolution: {integrity: sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==} + engines: {node: '>=v18'} + + '@commitlint/format@20.5.0': + resolution: {integrity: sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@20.5.0': + resolution: {integrity: sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==} + engines: {node: '>=v18'} + + '@commitlint/lint@20.5.0': + resolution: {integrity: sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==} + engines: {node: '>=v18'} + + '@commitlint/load@20.5.0': + resolution: {integrity: sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==} + engines: {node: '>=v18'} + + '@commitlint/message@20.4.3': + resolution: {integrity: sha512-6akwCYrzcrFcTYz9GyUaWlhisY4lmQ3KvrnabmhoeAV8nRH4dXJAh4+EUQ3uArtxxKQkvxJS78hNX2EU3USgxQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@20.5.0': + resolution: {integrity: sha512-SeKWHBMk7YOTnnEWUhx+d1a9vHsjjuo6Uo1xRfPNfeY4bdYFasCH1dDpAv13Lyn+dDPOels+jP6D2GRZqzc5fA==} + engines: {node: '>=v18'} + + '@commitlint/read@20.5.0': + resolution: {integrity: sha512-JDEIJ2+GnWpK8QqwfmW7O42h0aycJEWNqcdkJnyzLD11nf9dW2dWLTVEa8Wtlo4IZFGLPATjR5neA5QlOvIH1w==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@20.5.0': + resolution: {integrity: sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==} + engines: {node: '>=v18'} + + '@commitlint/rules@20.5.0': + resolution: {integrity: sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@20.0.0': + resolution: {integrity: sha512-2l9gmwiCRqZNWgV+pX1X7z4yP0b3ex/86UmUFgoRt672Ez6cAM2lOQeHFRUTuE6sPpi8XBCGnd8Kh3bMoyHwJw==} + engines: {node: '>=v18'} + + '@commitlint/top-level@20.4.3': + resolution: {integrity: sha512-qD9xfP6dFg5jQ3NMrOhG0/w5y3bBUsVGyJvXxdWEwBm8hyx4WOk3kKXw28T5czBYvyeCVJgJJ6aoJZUWDpaacQ==} + engines: {node: '>=v18'} + + '@commitlint/types@20.5.0': + resolution: {integrity: sha512-ZJoS8oSq2CAZEpc/YI9SulLrdiIyXeHb/OGqGrkUP6Q7YV+0ouNAa7GjqRdXeQPncHQIDz/jbCTlHScvYvO/gA==} + engines: {node: '>=v18'} + + '@conventional-changelog/git-client@2.7.0': + resolution: {integrity: sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw==} + engines: {node: '>=18'} + peerDependencies: + conventional-commits-filter: ^5.0.0 + conventional-commits-parser: ^6.4.0 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1187,6 +1277,14 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@simple-libs/child-process-utils@1.0.2': + resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} + engines: {node: '>=18'} + + '@simple-libs/stream-utils@1.2.0': + resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} + engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1593,6 +1691,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1727,6 +1828,10 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1760,6 +1865,9 @@ packages: resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} engines: {node: '>= 6'} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -1774,6 +1882,19 @@ packages: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} + conventional-changelog-angular@8.3.1: + resolution: {integrity: sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==} + engines: {node: '>=18'} + + conventional-changelog-conventionalcommits@9.3.1: + resolution: {integrity: sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw==} + engines: {node: '>=18'} + + conventional-commits-parser@6.4.0: + resolution: {integrity: sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==} + engines: {node: '>=18'} + hasBin: true + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1787,6 +1908,14 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + cosmiconfig@8.3.6: resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} engines: {node: '>=14'} @@ -1796,6 +1925,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.1: + resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -1853,6 +1991,10 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} @@ -1993,6 +2135,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + environment@1.1.0: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} @@ -2241,6 +2387,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.5.0: resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} @@ -2256,6 +2406,11 @@ packages: get-tsconfig@4.13.7: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + git-raw-commits@5.0.1: + resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} + engines: {node: '>=18'} + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2275,6 +2430,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -2316,6 +2475,11 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2331,6 +2495,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -2342,6 +2509,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2373,10 +2544,18 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -2562,9 +2741,27 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -2604,6 +2801,10 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + meow@13.2.0: + resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2927,6 +3128,10 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2935,6 +3140,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3465,6 +3674,10 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -3476,6 +3689,10 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} @@ -3485,6 +3702,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -3582,6 +3803,128 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@commitlint/cli@20.5.0(@types/node@20.19.39)(conventional-commits-parser@6.4.0)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 20.5.0 + '@commitlint/lint': 20.5.0 + '@commitlint/load': 20.5.0(@types/node@20.19.39)(typescript@5.9.3) + '@commitlint/read': 20.5.0(conventional-commits-parser@6.4.0) + '@commitlint/types': 20.5.0 + tinyexec: 1.1.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-conventionalcommits: 9.3.1 + + '@commitlint/config-validator@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + ajv: 8.18.0 + + '@commitlint/ensure@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@20.0.0': {} + + '@commitlint/format@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + semver: 7.7.4 + + '@commitlint/lint@20.5.0': + dependencies: + '@commitlint/is-ignored': 20.5.0 + '@commitlint/parse': 20.5.0 + '@commitlint/rules': 20.5.0 + '@commitlint/types': 20.5.0 + + '@commitlint/load@20.5.0(@types/node@20.19.39)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/execute-rule': 20.0.0 + '@commitlint/resolve-extends': 20.5.0 + '@commitlint/types': 20.5.0 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) + is-plain-obj: 4.1.0 + lodash.mergewith: 4.6.2 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@20.4.3': {} + + '@commitlint/parse@20.5.0': + dependencies: + '@commitlint/types': 20.5.0 + conventional-changelog-angular: 8.3.1 + conventional-commits-parser: 6.4.0 + + '@commitlint/read@20.5.0(conventional-commits-parser@6.4.0)': + dependencies: + '@commitlint/top-level': 20.4.3 + '@commitlint/types': 20.5.0 + git-raw-commits: 5.0.1(conventional-commits-parser@6.4.0) + minimist: 1.2.8 + tinyexec: 1.1.1 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@20.5.0': + dependencies: + '@commitlint/config-validator': 20.5.0 + '@commitlint/types': 20.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@20.5.0': + dependencies: + '@commitlint/ensure': 20.5.0 + '@commitlint/message': 20.4.3 + '@commitlint/to-lines': 20.0.0 + '@commitlint/types': 20.5.0 + + '@commitlint/to-lines@20.0.0': {} + + '@commitlint/top-level@20.4.3': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@20.5.0': + dependencies: + conventional-commits-parser: 6.4.0 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@2.7.0(conventional-commits-parser@6.4.0)': + dependencies: + '@simple-libs/child-process-utils': 1.0.2 + '@simple-libs/stream-utils': 1.2.0 + semver: 7.7.4 + optionalDependencies: + conventional-commits-parser: 6.4.0 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -4329,6 +4672,12 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@simple-libs/child-process-utils@1.0.2': + dependencies: + '@simple-libs/stream-utils': 1.2.0 + + '@simple-libs/stream-utils@1.2.0': {} + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -4763,6 +5112,8 @@ snapshots: argparse@2.0.1: {} + array-ify@1.0.0: {} + array-timsort@1.0.3: {} array-union@2.1.0: {} @@ -4891,6 +5242,12 @@ snapshots: cli-width@4.1.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@2.0.1: @@ -4916,6 +5273,11 @@ snapshots: array-timsort: 1.0.3 esprima: 4.0.1 + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + component-emitter@1.3.1: {} concat-map@0.0.1: {} @@ -4924,6 +5286,19 @@ snapshots: content-disposition@1.1.0: {} + conventional-changelog-angular@8.3.1: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@9.3.1: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@6.4.0: + dependencies: + '@simple-libs/stream-utils': 1.2.0 + meow: 13.2.0 + convert-source-map@2.0.0: {} cookie@1.1.1: {} @@ -4932,6 +5307,13 @@ snapshots: core-util-is@1.0.3: {} + cosmiconfig-typescript-loader@6.3.0(@types/node@20.19.39)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 20.19.39 + cosmiconfig: 9.0.1(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + cosmiconfig@8.3.6(typescript@5.9.3): dependencies: import-fresh: 3.3.1 @@ -4941,6 +5323,15 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cosmiconfig@9.0.1(typescript@5.9.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.9.3 + create-require@1.1.1: {} cross-spawn@7.0.6: @@ -4984,6 +5375,10 @@ snapshots: dependencies: esutils: 2.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + dotenv-expand@12.0.3: dependencies: dotenv: 16.6.1 @@ -5045,6 +5440,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + env-paths@2.2.1: {} + environment@1.1.0: {} error-ex@1.3.4: @@ -5408,6 +5805,8 @@ snapshots: function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: @@ -5432,6 +5831,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + git-raw-commits@5.0.1(conventional-commits-parser@6.4.0): + dependencies: + '@conventional-changelog/git-client': 2.7.0(conventional-commits-parser@6.4.0) + meow: 13.2.0 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5457,6 +5864,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -5498,6 +5909,8 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + husky@9.1.7: {} + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5511,6 +5924,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} inflight@1.0.6: @@ -5520,6 +5935,8 @@ snapshots: inherits@2.0.4: {} + ini@4.1.1: {} + ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -5540,8 +5957,12 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-path-inside@3.0.3: {} + is-plain-obj@4.1.0: {} + is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -5569,8 +5990,7 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jiti@2.6.1: - optional: true + jiti@2.6.1: {} js-tokens@10.0.0: {} @@ -5698,8 +6118,20 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + + lodash.kebabcase@4.1.1: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.upperfirst@4.3.1: {} + lodash@4.18.1: {} log-symbols@4.1.0: @@ -5743,6 +6175,8 @@ snapshots: dependencies: fs-monkey: 1.1.0 + meow@13.2.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6043,10 +6477,14 @@ snapshots: reflect-metadata@0.2.2: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} + resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} restore-cursor@3.1.0: @@ -6570,6 +7008,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -6580,10 +7024,22 @@ snapshots: xtend@4.0.2: {} + y18n@5.0.8: {} + yaml@2.8.3: {} yargs-parser@21.1.1: {} + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yn@3.1.1: {} yocto-queue@0.1.0: {} From c951454305c7d6317baab77a6fd7442c76241f9a Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 00:01:59 +0300 Subject: [PATCH 15/47] chore: bump docker pnpm --- Dockerfile.prod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.prod b/Dockerfile.prod index 16325c9..7112e09 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -18,7 +18,7 @@ COPY . . RUN pnpm run build RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ - pnpm prune --prod + pnpm prune --prod --ignore-scripts FROM node:20-alpine AS runner WORKDIR /app From 0bf105641aef56d26646838d50124ff8b6b3ec65 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 01:37:38 +0300 Subject: [PATCH 16/47] feat: add users module and standardized API error responses --- libs/bootstrap/src/setups/swagger.ts | 5 +- package.json | 1 + pnpm-lock.yaml | 29 ++++++ src/modules/app/app.module.ts | 7 +- src/modules/user/controller/index.ts | 1 + .../user/controller/user.controller.ts | 94 +++++++++++++++++++ src/modules/user/controller/user.swagger.ts | 30 ++++++ src/modules/user/dtos/index.ts | 1 + src/modules/user/dtos/user.dto.ts | 53 +++++++++++ src/modules/user/entities/index.ts | 1 + src/modules/user/entities/user.entity.ts | 56 +++++++++++ src/modules/user/index.ts | 2 + src/modules/user/repository/index.ts | 1 + .../repository/user.repository.interface.ts | 1 + .../user/repository/user.repository.ts | 12 +++ src/modules/user/user.module.ts | 17 ++++ src/modules/user/user.service.ts | 4 + src/shared/entities/index.ts | 1 + src/shared/error/filter.ts | 50 ++++++++++ src/shared/error/index.ts | 2 + src/shared/error/schema.ts | 57 +++++++++++ src/shared/error/swagger.ts | 30 ++++++ 22 files changed, 452 insertions(+), 3 deletions(-) create mode 100644 src/modules/user/controller/index.ts create mode 100644 src/modules/user/controller/user.controller.ts create mode 100644 src/modules/user/controller/user.swagger.ts create mode 100644 src/modules/user/dtos/index.ts create mode 100644 src/modules/user/dtos/user.dto.ts create mode 100644 src/modules/user/entities/index.ts create mode 100644 src/modules/user/entities/user.entity.ts create mode 100644 src/modules/user/index.ts create mode 100644 src/modules/user/repository/index.ts create mode 100644 src/modules/user/repository/user.repository.interface.ts create mode 100644 src/modules/user/repository/user.repository.ts create mode 100644 src/modules/user/user.module.ts create mode 100644 src/modules/user/user.service.ts create mode 100644 src/shared/error/filter.ts create mode 100644 src/shared/error/index.ts create mode 100644 src/shared/error/schema.ts create mode 100644 src/shared/error/swagger.ts diff --git a/libs/bootstrap/src/setups/swagger.ts b/libs/bootstrap/src/setups/swagger.ts index 90e938f..58d79af 100644 --- a/libs/bootstrap/src/setups/swagger.ts +++ b/libs/bootstrap/src/setups/swagger.ts @@ -3,6 +3,7 @@ import { cleanupOpenApiDoc } from 'nestjs-zod'; import type { NestFastifyApplication } from '@nestjs/platform-fastify'; import type { SwaggerOptions } from '../interfaces'; import { SWAGGER_DEFAULTS } from '../configs/swagger'; +import { GlobalErrorResponse } from 'src/shared/error/schema'; export async function setupSwagger(app: NestFastifyApplication, options: SwaggerOptions = {}) { const { title, description, version, path, server } = { @@ -22,7 +23,9 @@ export async function setupSwagger(app: NestFastifyApplication, options: Swagger if (stage) builder.addServer(`https://api.${stage}`, 'Staging'); if (domain) builder.addServer(`https://api.${domain}`, 'Production'); - const document = SwaggerModule.createDocument(app, builder.build()); + const document = SwaggerModule.createDocument(app, builder.build(), { + extraModels: [GlobalErrorResponse.Output], + }); SwaggerModule.setup(path, app, cleanupOpenApiDoc(document), { jsonDocumentUrl: `${path}/s/json`, diff --git a/package.json b/package.json index 3aee9e1..318020b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/platform-fastify": "^11.1.18", "@nestjs/swagger": "^11.2.7", "@nestjs/throttler": "^6.5.0", + "@paralleldrive/cuid2": "^3.3.0", "@willsoto/nestjs-prometheus": "^6.1.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91a0420..1d033cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@nestjs/throttler': specifier: ^6.5.0 version: 6.5.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + '@paralleldrive/cuid2': + specifier: ^3.3.0 + version: 3.3.0 '@willsoto/nestjs-prometheus': specifier: ^6.1.0 version: 6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) @@ -1133,6 +1136,10 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1160,6 +1167,10 @@ packages: '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@paralleldrive/cuid2@3.3.0': + resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} + hasBin: true + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1736,6 +1747,9 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} @@ -2143,6 +2157,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + error-causes@3.0.2: + resolution: {integrity: sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw==} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -4585,6 +4602,8 @@ snapshots: '@noble/hashes@1.8.0': {} + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4609,6 +4628,12 @@ snapshots: dependencies: '@noble/hashes': 1.8.0 + '@paralleldrive/cuid2@3.3.0': + dependencies: + '@noble/hashes': 2.0.1 + bignumber.js: 9.3.1 + error-causes: 3.0.2 + '@pinojs/redact@0.4.0': {} '@pkgr/core@0.2.9': {} @@ -5145,6 +5170,8 @@ snapshots: baseline-browser-mapping@2.10.17: {} + bignumber.js@9.3.1: {} + bintrees@1.0.2: {} bl@4.1.0: @@ -5444,6 +5471,8 @@ snapshots: environment@1.1.0: {} + error-causes@3.0.2: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index c55df26..0e4c27f 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -5,9 +5,11 @@ import { DatabaseModule } from '@libs/database'; import { ConfigService } from '@nestjs/config'; import * as schema from '../../shared/entities'; import { APP_FILTER, APP_PIPE } from '@nestjs/core'; -import { ZodValidationPipe, ZodValidationException } from 'nestjs-zod'; +import { ZodValidationPipe } from 'nestjs-zod'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { HealthModule } from '@libs/health'; +import { UserModule } from '../user'; +import { GlobalExceptionFilter } from 'src/shared/error'; @Module({ imports: [ @@ -31,6 +33,7 @@ import { HealthModule } from '@libs/health'; }; }, }), + UserModule, HealthModule.register('gateway'), ], controllers: [AppController], @@ -41,7 +44,7 @@ import { HealthModule } from '@libs/health'; }, { provide: APP_FILTER, - useClass: ZodValidationException, + useClass: GlobalExceptionFilter, }, ], }) diff --git a/src/modules/user/controller/index.ts b/src/modules/user/controller/index.ts new file mode 100644 index 0000000..07eed15 --- /dev/null +++ b/src/modules/user/controller/index.ts @@ -0,0 +1 @@ +export { UserController } from './user.controller'; diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts new file mode 100644 index 0000000..ce140fe --- /dev/null +++ b/src/modules/user/controller/user.controller.ts @@ -0,0 +1,94 @@ +import { Body, Controller, Get, Patch, Post, Query } from '@nestjs/common'; +import { UserService } from '../user.service'; +import { createId } from '@paralleldrive/cuid2'; +import { GetMeSwagger } from './user.swagger'; +import { ApiTags } from '@nestjs/swagger'; +import { UpdateNotificationsDto, UpdateProfileDto } from '../dtos'; + +@ApiTags('Users') +@Controller('users') +export class UserController { + constructor(private readonly facade: UserService) {} + + @Get('me') + @GetMeSwagger() + async getProfile() { + return { + id: createId(), + fullName: 'Alexey Smirnov', + email: 'alexey.smirnov@example.com', + bio: 'Менеджер продукта с 5-летним опытом создания SaaS-платформ. Увлечён продуктивностью и чистым дизайном.', + avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', + timezone: 'Europe/Moscow', + language: 'ru', + security: { + is2faEnabled: true, + lastPasswordChange: new Date('2023-10-24').toISOString(), + }, + notifications: { + email: { task_assigned: true, mentions: true, daily_summary: false }, + push: { task_assigned: true, reminders: true }, + }, + }; + } + + @Patch('me') + async updateProfile(@Body() dto: UpdateProfileDto) { + return { + success: true, + message: 'Profile updated successfully', + updatedAt: new Date().toISOString(), + data: dto, + }; + } + + @Patch('me/notifications') + async updateNotifications(@Body() settings: UpdateNotificationsDto) { + return { + success: true, + newSettings: settings, + }; + } + + @Get('me/activity') + async getActivity(@Query('limit') limit: string) { + return [ + { + id: createId(), + eventType: 'TASK_COMPLETED', + description: 'Завершена задача "Обновить текст лендинга"', + createdAt: new Date(Date.now() - 1000 * 60 * 120).toISOString(), + metadata: { taskId: createId() }, + }, + { + id: createId(), + eventType: 'SECURITY_UPDATE', + description: 'Вы изменили настройки пароля', + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), + metadata: null, + }, + { + id: createId(), + eventType: 'COMMENT_ADDED', + description: 'Вы прокомментировали "Проверка дизайн-системы"', + createdAt: '2023-10-24T14:30:00Z', + metadata: { taskId: createId() }, + }, + { + id: createId(), + eventType: 'AVATAR_UPLOADED', + description: 'Вы загрузили новую фотографию профиля', + createdAt: '2023-10-22T10:00:00Z', + metadata: null, + }, + ].slice(0, Number(limit) || 10); + } + + @Post('me/avatar') + async uploadAvatar() { + return { + avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', + success: true, + }; + } +} diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts new file mode 100644 index 0000000..8e2428d --- /dev/null +++ b/src/modules/user/controller/user.swagger.ts @@ -0,0 +1,30 @@ +import { ApiExtraModels, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { UserResponse } from '../dtos'; +import { applyDecorators } from '@nestjs/common'; +import { ApiErrorResponse } from 'src/shared/error'; + +export const GetMeSwagger = () => + applyDecorators( + ApiExtraModels(UserResponse.Output), + ApiOperation({ + summary: 'Получить профиль текущего пользователя', + description: + 'Возвращает полную структуру профиля, включая вложенные объекты безопасности и настроек.', + }), + ApiResponse({ + status: 200, + description: 'Данные профиля успешно получены.', + type: UserResponse.Output, + }), + ApiErrorResponse(400, 'VALIDATION_FAILED', 'Ошибка во входных данных', [ + { field: 'email', message: 'Некорректный формат почты', code: 'invalid_email' }, + ]), + ApiErrorResponse(401, 'AUTH_REQUIRED', 'Сессия истекла или токен не валиден'), + ApiErrorResponse(403, 'ACCESS_DENIED', 'У вас недостаточно прав для этого действия'), + ApiErrorResponse(404, 'USER_NOT_FOUND', 'Пользователь не найден в базе данных'), + ApiErrorResponse( + 500, + 'INTERNAL_SERVER_ERROR', + 'Произошла критическая ошибка на стороне сервера', + ), + ); diff --git a/src/modules/user/dtos/index.ts b/src/modules/user/dtos/index.ts new file mode 100644 index 0000000..bdcec8b --- /dev/null +++ b/src/modules/user/dtos/index.ts @@ -0,0 +1 @@ +export { UpdateProfileDto, UpdateNotificationsDto, UserResponse } from './user.dto'; diff --git a/src/modules/user/dtos/user.dto.ts b/src/modules/user/dtos/user.dto.ts new file mode 100644 index 0000000..5d47ec7 --- /dev/null +++ b/src/modules/user/dtos/user.dto.ts @@ -0,0 +1,53 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +const NotificationsSchema = z + .object({ + email: z.object({ + task_assigned: z.boolean().describe('Уведомление на почту при назначении задачи'), + mentions: z.boolean().describe('Уведомление на почту при упоминании в комментариях'), + daily_summary: z.boolean().describe('Ежедневная сводка задач на почту'), + }), + push: z.object({ + task_assigned: z.boolean().describe('Push-уведомление при назначении задачи'), + reminders: z.boolean().describe('Push-уведомления о дедлайнах'), + }), + }) + .describe('Настройки уведомлений пользователя'); + +export const UpdateNotificationsSchema = NotificationsSchema.partial().describe( + 'Схема для частичного обновления настроек уведомлений', +); + +export class UpdateNotificationsDto extends createZodDto(UpdateNotificationsSchema) {} + +const SecuritySchema = z + .object({ + is2faEnabled: z.boolean().describe('Статус двухфакторной аутентификации'), + lastPasswordChange: z.string().datetime().describe('Дата последнего изменения пароля'), + }) + .describe('Данные безопасности аккаунта'); + +export const UserSchema = z.object({ + id: z.string().cuid2().describe('Уникальный идентификатор пользователя (CUID2)'), + fullName: z.string().min(2).max(255).describe('Полное имя пользователя'), + email: z.string().email().describe('Электронная почта'), + bio: z.string().max(1000).nullable().describe('Информация "О себе"'), + avatarUrl: z.string().url().describe('Ссылка на аватарку пользователя'), + timezone: z.string().describe('Временная зона пользователя (IANA формат)'), + language: z.enum(['ru', 'en']).describe('Выбранный язык интерфейса'), + security: SecuritySchema, + notifications: NotificationsSchema, +}); +export class UserResponse extends createZodDto(UserSchema) {} + +export const UpdateProfileSchema = UserSchema.pick({ + fullName: true, + bio: true, + timezone: true, + language: true, +}) + .partial() + .describe('Схема для частичного обновления профиля'); + +export class UpdateProfileDto extends createZodDto(UpdateProfileSchema) {} diff --git a/src/modules/user/entities/index.ts b/src/modules/user/entities/index.ts new file mode 100644 index 0000000..426faed --- /dev/null +++ b/src/modules/user/entities/index.ts @@ -0,0 +1 @@ +export { userActivity, userNotifications, userSecurity, users } from './user.entity'; diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts new file mode 100644 index 0000000..3f4955b --- /dev/null +++ b/src/modules/user/entities/user.entity.ts @@ -0,0 +1,56 @@ +import { createId } from '@paralleldrive/cuid2'; +import { varchar, text, timestamp, boolean, jsonb } from 'drizzle-orm/pg-core'; +import { baseSchema } from 'src/shared/entities'; + +export const users = baseSchema.table('users', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + fullName: varchar('full_name', { length: 255 }).notNull(), + email: varchar('email', { length: 255 }).notNull().unique(), + bio: text('bio'), + avatarUrl: varchar('avatar_url', { length: 512 }), + timezone: varchar('timezone', { length: 50 }).default('UTC').notNull(), + language: varchar('language', { length: 5 }).default('ru').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), +}); + +export const userSecurity = baseSchema.table('user_security', { + userId: text('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), + passwordHash: varchar('password_hash', { length: 255 }).notNull(), + is2faEnabled: boolean('is_2fa_enabled').default(false).notNull(), + twoFactorSecret: text('two_factor_secret'), + lastPasswordChange: timestamp('last_password_change', { withTimezone: true }) + .defaultNow() + .notNull(), +}); + +export const userNotifications = baseSchema.table('user_notifications', { + userId: text('user_id') + .primaryKey() + .references(() => users.id, { onDelete: 'cascade' }), + settings: jsonb('settings') + .$type<{ + email: { task_assigned: boolean; mentions: boolean; daily_summary: boolean }; + push: { task_assigned: boolean; reminders: boolean }; + }>() + .default({ + email: { task_assigned: true, mentions: true, daily_summary: false }, + push: { task_assigned: true, reminders: true }, + }) + .notNull(), +}); + +export const userActivity = baseSchema.table('user_activity', { + id: text('id').primaryKey(), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + eventType: varchar('event_type', { length: 50 }).notNull(), + entityId: varchar('entity_id'), + metadata: jsonb('metadata'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}); diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts new file mode 100644 index 0000000..2266068 --- /dev/null +++ b/src/modules/user/index.ts @@ -0,0 +1,2 @@ +export { UserModule } from './user.module'; +export { UserRepository } from './repository/user.repository'; diff --git a/src/modules/user/repository/index.ts b/src/modules/user/repository/index.ts new file mode 100644 index 0000000..3e89261 --- /dev/null +++ b/src/modules/user/repository/index.ts @@ -0,0 +1 @@ +export {} from './user.repository'; diff --git a/src/modules/user/repository/user.repository.interface.ts b/src/modules/user/repository/user.repository.interface.ts new file mode 100644 index 0000000..55d2f3a --- /dev/null +++ b/src/modules/user/repository/user.repository.interface.ts @@ -0,0 +1 @@ +export interface IUserRepository {} diff --git a/src/modules/user/repository/user.repository.ts b/src/modules/user/repository/user.repository.ts new file mode 100644 index 0000000..f1b942f --- /dev/null +++ b/src/modules/user/repository/user.repository.ts @@ -0,0 +1,12 @@ +import * as users from '../entities'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { IUserRepository } from './user.repository.interface'; +import { Inject, Injectable } from '@nestjs/common'; + +@Injectable() +export class UserRepository implements IUserRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly repository: DatabaseService, + ) {} +} diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..6540e72 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { UserController } from './controller'; +import { UserService } from './user.service'; +import { UserRepository } from './repository/user.repository'; + +const REPOSITORY = { + provide: 'IUserRepository', + useClass: UserRepository, +}; + +@Module({ + imports: [], + controllers: [UserController], + providers: [REPOSITORY, UserService], + exports: [REPOSITORY], +}) +export class UserModule {} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts new file mode 100644 index 0000000..668a7d6 --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UserService {} diff --git a/src/shared/entities/index.ts b/src/shared/entities/index.ts index 9d8074b..3c977e4 100644 --- a/src/shared/entities/index.ts +++ b/src/shared/entities/index.ts @@ -1 +1,2 @@ export { baseSchema } from './schema'; +export * from '../../modules/user/entities'; diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts new file mode 100644 index 0000000..4cbf29b --- /dev/null +++ b/src/shared/error/filter.ts @@ -0,0 +1,50 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; +import { createId } from '@paralleldrive/cuid2'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + // 1. Определяем статус + let status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + let details = []; + let message = exception.message; + let code = 'INTERNAL_ERROR'; + + if (exception?.name === 'ZodValidationException') { + status = 400; + code = 'VALIDATION_FAILED'; + details = exception.getResponse()?.errors || []; + message = 'Validation failed'; + } else if (exception instanceof HttpException) { + const res = exception.getResponse() as any; + code = res.code || 'HTTP_ERROR'; + details = res.details || []; + } + + const requestId = request.headers['x-request-id'] || createId(); + + const errorResponse = { + code, + message, + retryable: status >= 500, + details, + meta: { + requestId, + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + service: 'main-api', + }, + }; + + response.status(status).json(errorResponse); + } +} diff --git a/src/shared/error/index.ts b/src/shared/error/index.ts new file mode 100644 index 0000000..544657a --- /dev/null +++ b/src/shared/error/index.ts @@ -0,0 +1,2 @@ +export * from './swagger'; +export * from './filter'; diff --git a/src/shared/error/schema.ts b/src/shared/error/schema.ts new file mode 100644 index 0000000..20e2a8b --- /dev/null +++ b/src/shared/error/schema.ts @@ -0,0 +1,57 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +const ErrorDetailSchema = z + .object({ + field: z.string().describe('Путь к полю в формате dot-notation (например, "user.email")'), + message: z.string().describe('Человекочитаемое сообщение о конкретной ошибке в этом поле'), + code: z + .string() + .describe( + 'Машиночитаемый код ошибки валидации (например, "invalid_email", "too_short")', + ), + }) + .describe('Детальная информация о конкретном нарушении в запросе'); + +const ErrorMetaSchema = z + .object({ + requestId: z + .string() + .describe( + 'Уникальный ID запроса (Trace ID). Используется для поиска логов в Sentry/ELK/Kibana', + ), + timestamp: z + .string() + .datetime() + .describe('Точное время возникновения ошибки в формате ISO 8601'), + path: z.string().describe('URL-путь эндпоинта, который вернул ошибку'), + method: z.string().describe('HTTP метод запроса (GET, POST, etc.)'), + service: z + .string() + .optional() + .describe( + 'Имя микросервиса, в котором произошел сбой (полезно для будущего масштабирования)', + ), + }) + .describe('Техническая мета-информация для мониторинга и отладки'); + +export const GlobalErrorSchema = z.object({ + code: z + .string() + .describe( + 'Уникальный бизнес-код ошибки (например, "INSUFFICIENT_FUNDS", "TEAM_NOT_FOUND")', + ), + message: z.string().describe('Краткое описание ошибки для пользователя или разработчика'), + retryable: z + .boolean() + .describe( + 'Флаг, указывающий клиенту, есть ли смысл повторять запрос без изменений (например, при 503 или Lock Timeout)', + ), + details: z + .array(ErrorDetailSchema) + .optional() + .describe('Список ошибок валидации (заполняется только для 400 ошибок)'), + meta: ErrorMetaSchema, +}); + +export class GlobalErrorResponse extends createZodDto(GlobalErrorSchema) {} diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts new file mode 100644 index 0000000..4f582ce --- /dev/null +++ b/src/shared/error/swagger.ts @@ -0,0 +1,30 @@ +import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; +import { GlobalErrorResponse } from './schema'; + +export const ApiErrorResponse = ( + status: number, + bizCode: string, + description: string, + details?: { field: string; message: string; code: string }[], +) => { + return ApiResponse({ + status, + description, + schema: { + allOf: [{ $ref: getSchemaPath(GlobalErrorResponse.Output) }], + example: { + code: bizCode, + message: description, + retryable: status >= 500, + details: details || [], + meta: { + requestId: 'req-clj1abc230000jk78', + timestamp: new Date().toISOString(), + path: '/api/v1/...', + method: 'POST', + service: 'main-backend', + }, + }, + }, + }); +}; From ebf5bf0ae88b21ba0cfa8474f6022ac31cdfb650 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 02:09:57 +0300 Subject: [PATCH 17/47] feat: core domain entity type / add service and repository --- src/modules/user/entities/user.domain.ts | 19 +++ .../repository/user.repository.interface.ts | 35 ++++- .../user/repository/user.repository.ts | 141 +++++++++++++++++- src/modules/user/user.service.ts | 92 +++++++++++- 4 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 src/modules/user/entities/user.domain.ts diff --git a/src/modules/user/entities/user.domain.ts b/src/modules/user/entities/user.domain.ts new file mode 100644 index 0000000..ffd0966 --- /dev/null +++ b/src/modules/user/entities/user.domain.ts @@ -0,0 +1,19 @@ +import { InferSelectModel, InferInsertModel } from 'drizzle-orm'; +import { users, userSecurity, userNotifications, userActivity } from './user.entity'; + +export type User = InferSelectModel; +export type NewUser = InferInsertModel; + +export type UserSecurity = InferSelectModel; +export type NewUserSecurity = InferInsertModel; + +export type UserNotifications = InferSelectModel; +export type NotificationSettings = UserNotifications['settings']; + +export type UserActivity = InferSelectModel; +export type NewUserActivity = InferInsertModel; + +export type UserProfile = User & { + security: Omit; + notifications: UserNotifications['settings']; +}; diff --git a/src/modules/user/repository/user.repository.interface.ts b/src/modules/user/repository/user.repository.interface.ts index 55d2f3a..5121fd0 100644 --- a/src/modules/user/repository/user.repository.interface.ts +++ b/src/modules/user/repository/user.repository.interface.ts @@ -1 +1,34 @@ -export interface IUserRepository {} +import { + NewUser, + NewUserActivity, + User, + UserActivity, + UserNotifications, + UserProfile, +} from '../entities/user.domain'; + +export interface IUserRepository { + findById(id: string): Promise; + findByEmail(email: string): Promise; + + existsByEmail(email: string): Promise; + + updateProfile(id: string, data: Partial): Promise; + updateNotifications(id: string, settings: UserNotifications['settings']): Promise; + updateAvatar(id: string, url: string): Promise; + + create(data: NewUser): Promise; + + logActivity(data: NewUserActivity): Promise; + findActivityByUser( + userId: string, + options: { limit: number; offset: number }, + ): Promise<{ + items: UserActivity[]; + total: number; + }>; + + findProfile(id: string): Promise; + + updatePasswordHash(id: string, hash: string): Promise; +} diff --git a/src/modules/user/repository/user.repository.ts b/src/modules/user/repository/user.repository.ts index f1b942f..9bd0ab0 100644 --- a/src/modules/user/repository/user.repository.ts +++ b/src/modules/user/repository/user.repository.ts @@ -1,12 +1,149 @@ -import * as users from '../entities'; +import * as sc from '../entities'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { IUserRepository } from './user.repository.interface'; import { Inject, Injectable } from '@nestjs/common'; +import { NewUser, NewUserActivity, User, UserNotifications } from '../entities/user.domain'; +import { createId } from '@paralleldrive/cuid2'; +import { desc, eq, count } from 'drizzle-orm'; @Injectable() export class UserRepository implements IUserRepository { constructor( @Inject(DATABASE_SERVICE) - private readonly repository: DatabaseService, + private readonly repository: DatabaseService, ) {} + + async findProfile(id: string) { + const rows = await this.repository + .select() + .from(sc.users) + .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) + .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)) + .where(eq(sc.users.id, id)) + .limit(1); + + if (rows.length === 0) return null; + + const { users: user, user_security: security, user_notifications: notifications } = rows[0]; + + return { + ...user, + security: { + is2faEnabled: security?.is2faEnabled ?? false, + lastPasswordChange: security?.lastPasswordChange ?? user.createdAt, + }, + notifications: notifications?.settings ?? { + email: { task_assigned: true, mentions: true, daily_summary: false }, + push: { task_assigned: true, reminders: true }, + }, + }; + } + + async findById(id: string): Promise { + const [result] = await this.repository + .select() + .from(sc.users) + .where(eq(sc.users.id, id)) + .limit(1); + return result || null; + } + + async findByEmail(email: string): Promise { + const [result] = await this.repository + .select() + .from(sc.users) + .where(eq(sc.users.email, email)) + .limit(1); + return result || null; + } + + async findSecurityByUserId(userId: string) { + const [result] = await this.repository + .select() + .from(sc.userSecurity) + .where(eq(sc.userSecurity.userId, userId)) + .limit(1); + return result || null; + } + + async create(data: NewUser): Promise { + return await this.repository.transaction(async (tx) => { + const [newUser] = await tx.insert(sc.users).values(data).returning(); + + await tx.insert(sc.userNotifications).values({ + userId: newUser.id, + }); + + return newUser; + }); + } + + async existsByEmail(email: string): Promise { + const [result] = await this.repository + .select({ value: count() }) + .from(sc.users) + .where(eq(sc.users.email, email)); + return (result?.value ?? 0) > 0; + } + + async updateProfile(id: string, data: Partial): Promise { + const [updated] = await this.repository + .update(sc.users) + .set({ ...data, updatedAt: new Date() }) + .where(eq(sc.users.id, id)) + .returning(); + return updated; + } + + async updateNotifications(id: string, settings: UserNotifications['settings']) { + await this.repository + .update(sc.userNotifications) + .set({ settings }) + .where(eq(sc.userNotifications.userId, id)); + } + + async updateAvatar(id: string, url: string) { + await this.repository + .update(sc.users) + .set({ avatarUrl: url, updatedAt: new Date() }) + .where(eq(sc.users.id, id)); + } + + async updatePasswordHash(id: string, hash: string) { + await this.repository + .insert(sc.userSecurity) + .values({ userId: id, passwordHash: hash }) + .onConflictDoUpdate({ + target: sc.userSecurity.userId, + set: { passwordHash: hash, lastPasswordChange: new Date() }, + }); + } + + async logActivity(data: NewUserActivity) { + await this.repository.insert(sc.userActivity).values({ + ...data, + id: data.id ?? createId(), + }); + } + + async findActivityByUser(userId: string, options: { limit: number; offset: number }) { + const [totalResult, items] = await Promise.all([ + this.repository + .select({ value: count() }) + .from(sc.userActivity) + .where(eq(sc.userActivity.userId, userId)), + this.repository + .select() + .from(sc.userActivity) + .where(eq(sc.userActivity.userId, userId)) + .limit(options.limit) + .offset(options.offset) + .orderBy(desc(sc.userActivity.createdAt)), + ]); + + return { + items, + total: Number(totalResult[0]?.value ?? 0), + }; + } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 668a7d6..57a283f 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,4 +1,92 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { IUserRepository } from './repository/user.repository.interface'; +import { UpdateNotificationsDto, UpdateProfileDto } from './dtos'; +import { createId } from '@paralleldrive/cuid2'; @Injectable() -export class UserService {} +export class UserService { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + ) {} + + private throwUserNotFound() { + throw new NotFoundException({ + code: 'USER_NOT_FOUND', + message: 'Пользователь не найден в системе', + }); + } + + public getProfile = async (id: string) => { + const user = await this.userRepo.findProfile(id); + if (!user) this.throwUserNotFound(); + return user; + }; + + public updateProfile = async (id: string, dto: UpdateProfileDto) => { + const user = await this.userRepo.findById(id); + if (!user) this.throwUserNotFound(); + + const updatedUser = await this.userRepo.updateProfile(id, dto); + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'PROFILE_UPDATED', + metadata: { fields: Object.keys(dto) }, + }); + + return updatedUser; + }; + + public updateNotifications = async (id: string, dto: UpdateNotificationsDto) => { + const user = await this.userRepo.findById(id); + + if (!user) this.throwUserNotFound(); + + await this.userRepo.updateNotifications(id, { + email: dto.email, + push: dto.push, + }); + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'NOTIFICATIONS_UPDATED', + }); + + return { success: true }; + }; + + public getActivity = async (id: string, page: number = 1, limit: number = 20) => { + const safeLimit = Math.min(limit, 50); + const offset = (page - 1) * safeLimit; + + return await this.userRepo.findActivityByUser(id, { + limit: safeLimit, + offset, + }); + }; + + public uploadAvatar = async (id: string, avatarUrl: string) => { + try { + new URL(avatarUrl); + } catch { + throw new BadRequestException({ + code: 'INVALID_AVATAR_URL', + message: 'Предоставлен некорректный URL аватара', + }); + } + + await this.userRepo.updateAvatar(id, avatarUrl); + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'AVATAR_CHANGED', + metadata: { url: avatarUrl }, + }); + + return { avatarUrl, success: true }; + }; +} From 32f7f53d51e2e62e2fb1e89b8f557d2a49f8b7e6 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 02:24:54 +0300 Subject: [PATCH 18/47] chore: add auth module arch --- src/modules/app/app.module.ts | 2 ++ src/modules/auth/auth.module.ts | 10 ++++++++ src/modules/auth/auth.service.ts | 0 .../auth/controller/auth.controller.ts | 11 ++++++++ src/modules/auth/entities/index.ts | 1 + src/modules/auth/entities/session.entity.ts | 25 +++++++++++++++++++ src/modules/auth/index.ts | 1 + src/shared/entities/index.ts | 1 + 8 files changed, 51 insertions(+) create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/controller/auth.controller.ts create mode 100644 src/modules/auth/entities/index.ts create mode 100644 src/modules/auth/entities/session.entity.ts create mode 100644 src/modules/auth/index.ts diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index 0e4c27f..dc85c16 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -10,6 +10,7 @@ import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { HealthModule } from '@libs/health'; import { UserModule } from '../user'; import { GlobalExceptionFilter } from 'src/shared/error'; +import { AuthModule } from '../auth'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { GlobalExceptionFilter } from 'src/shared/error'; }; }, }), + AuthModule, UserModule, HealthModule.register('gateway'), ], diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..2815ef3 --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,10 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { UserModule } from '../user'; + +@Module({ + imports: [forwardRef(() => UserModule)], + controllers: [], + providers: [], + exports: [], +}) +export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts new file mode 100644 index 0000000..221b351 --- /dev/null +++ b/src/modules/auth/controller/auth.controller.ts @@ -0,0 +1,11 @@ +// POST /auth/register — Регистрация (создание записей в users, user_security, user_notifications). +// POST /auth/login — Вход (выдача Access/Refresh токенов). Если включен 2FA — возврат промежуточного токена. +// POST /auth/refresh — Обновление пары токенов через Refresh Token. +// POST /auth/logout — Удаление текущей сессии из Redis. +// GET /auth/sessions — Список всех активных устройств пользователя. +// DELETE /auth/sessions/:cuid — Принудительное завершение сессии на другом устройстве. +// POST /auth/change-password — Смена пароля (требует oldPassword и newPassword). +// Logic: При успехе обновляем lastPasswordChange и инвалидируем все сессии в Redis, кроме текущей. +// POST /auth/2fa/enable — Генерация QR-кода (возвращает otpauth ссылку). +// POST /auth/2fa/confirm — Подтверждение включения 2FA (проверка первого кода). +// PATCH /auth/2fa/disable — Отключение (обязательно под паролем или кодом). diff --git a/src/modules/auth/entities/index.ts b/src/modules/auth/entities/index.ts new file mode 100644 index 0000000..5330080 --- /dev/null +++ b/src/modules/auth/entities/index.ts @@ -0,0 +1 @@ +export { sessions } from './session.entity'; diff --git a/src/modules/auth/entities/session.entity.ts b/src/modules/auth/entities/session.entity.ts new file mode 100644 index 0000000..5b7414d --- /dev/null +++ b/src/modules/auth/entities/session.entity.ts @@ -0,0 +1,25 @@ +import { createId } from '@paralleldrive/cuid2'; +import { text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { boolean } from 'drizzle-orm/pg-core'; +import { baseSchema } from 'src/shared/entities'; +import { users } from '../../user/entities'; + +export const sessions = baseSchema.table('sessions', { + id: text('id') + .primaryKey() + .$defaultFn(() => createId()), + userId: text('user_id') + .references(() => users.id, { onDelete: 'cascade' }) + .notNull(), + deviceType: varchar('device_type', { length: 20 }).$type<'mobile' | 'desktop' | 'tablet'>(), + browser: varchar('browser', { length: 50 }), + os: varchar('os', { length: 50 }), + userAgent: text('user_agent').notNull(), + ip: varchar('ip', { length: 45 }).notNull(), + city: varchar('city', { length: 100 }), + countryCode: varchar('country_code', { length: 5 }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + isRevoked: boolean('is_revoked').default(false).notNull(), +}); diff --git a/src/modules/auth/index.ts b/src/modules/auth/index.ts new file mode 100644 index 0000000..faa5c33 --- /dev/null +++ b/src/modules/auth/index.ts @@ -0,0 +1 @@ +export { AuthModule } from './auth.module'; diff --git a/src/shared/entities/index.ts b/src/shared/entities/index.ts index 3c977e4..2e1f6bc 100644 --- a/src/shared/entities/index.ts +++ b/src/shared/entities/index.ts @@ -1,2 +1,3 @@ export { baseSchema } from './schema'; export * from '../../modules/user/entities'; +export * from '../../modules/auth/entities'; From d1dfd322d0f50b6b4f7f37934fa9ce252abbb4a1 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 03:00:17 +0300 Subject: [PATCH 19/47] feat(shared): add ApiBaseController decorator for global swagger responses --- src/shared/decorators/api-controller.decorator.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/shared/decorators/api-controller.decorator.ts diff --git a/src/shared/decorators/api-controller.decorator.ts b/src/shared/decorators/api-controller.decorator.ts new file mode 100644 index 0000000..d8c9d9c --- /dev/null +++ b/src/shared/decorators/api-controller.decorator.ts @@ -0,0 +1,15 @@ +import { Controller, applyDecorators } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { ApiErrorResponse } from 'src/shared/error'; + +export const ApiBaseController = (path: string, tag: string) => { + return applyDecorators( + ApiTags(tag), + Controller(path), + ApiErrorResponse( + 500, + 'INTERNAL_SERVER_ERROR', + 'Произошла критическая ошибка на стороне сервера', + ), + ); +}; From a6f81eb37733dd6f76c0573c6d6202a19a4c4180 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 03:54:27 +0300 Subject: [PATCH 20/47] feat(swagger): add reusable swagger error decorators --- src/shared/error/swagger.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts index 4f582ce..12d57ea 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -1,5 +1,6 @@ import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; import { GlobalErrorResponse } from './schema'; +import { applyDecorators } from '@nestjs/common'; export const ApiErrorResponse = ( status: number, @@ -28,3 +29,22 @@ export const ApiErrorResponse = ( }, }); }; + +export const ApiBadRequest = (description: string = 'Некорректный запрос') => + applyDecorators(ApiErrorResponse(400, 'BAD_REQUEST', description)); + +export const ApiRequireAuth = () => + applyDecorators(ApiErrorResponse(401, 'AUTH_REQUIRED', 'Сессия истекла или токен не валиден')); + +export const ApiForbidden = () => + applyDecorators( + ApiErrorResponse(403, 'ACCESS_DENIED', 'У вас недостаточно прав для этого действия'), + ); + +export const ApiNotFound = (description: string = 'Ресурс не найден') => + applyDecorators(ApiErrorResponse(404, 'NOT_FOUND', description)); + +export const ApiValidationError = ( + description: string = 'Ошибка валидации входных данных', + fields: any[] = [], +) => applyDecorators(ApiErrorResponse(400, 'VALIDATION_FAILED', description, fields)); From 525449ccf8ef00f9e65709063222112dfc6f57f0 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 04:00:04 +0300 Subject: [PATCH 21/47] docs(users): describe user api endpoints in swagger --- .../user/controller/user.controller.ts | 21 ++- src/modules/user/controller/user.swagger.ts | 141 ++++++++++++++++-- src/shared/decorators/index.ts | 1 + 3 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 src/shared/decorators/index.ts diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts index ce140fe..fb84dd5 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/modules/user/controller/user.controller.ts @@ -1,12 +1,17 @@ -import { Body, Controller, Get, Patch, Post, Query } from '@nestjs/common'; +import { Body, Get, Patch, Post, Query } from '@nestjs/common'; import { UserService } from '../user.service'; import { createId } from '@paralleldrive/cuid2'; -import { GetMeSwagger } from './user.swagger'; -import { ApiTags } from '@nestjs/swagger'; +import { + GetMeActivitySwagger, + GetMeSwagger, + PatchMeNotificationsSwagger, + PatchMeSwagger, + PostMeAvatarSwagger, +} from './user.swagger'; import { UpdateNotificationsDto, UpdateProfileDto } from '../dtos'; +import { ApiBaseController } from '../../../shared/decorators'; -@ApiTags('Users') -@Controller('users') +@ApiBaseController('users', 'Users') export class UserController { constructor(private readonly facade: UserService) {} @@ -33,16 +38,18 @@ export class UserController { } @Patch('me') + @PatchMeSwagger() async updateProfile(@Body() dto: UpdateProfileDto) { return { success: true, - message: 'Profile updated successfully', + message: 'Профиль успешно обновлен.', updatedAt: new Date().toISOString(), data: dto, }; } @Patch('me/notifications') + @PatchMeNotificationsSwagger() async updateNotifications(@Body() settings: UpdateNotificationsDto) { return { success: true, @@ -51,6 +58,7 @@ export class UserController { } @Get('me/activity') + @GetMeActivitySwagger() async getActivity(@Query('limit') limit: string) { return [ { @@ -85,6 +93,7 @@ export class UserController { } @Post('me/avatar') + @PostMeAvatarSwagger() async uploadAvatar() { return { avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts index 8e2428d..2700ebb 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/modules/user/controller/user.swagger.ts @@ -1,7 +1,14 @@ -import { ApiExtraModels, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { UserResponse } from '../dtos'; +import { + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiOperation, + ApiQuery, + ApiResponse, +} from '@nestjs/swagger'; +import { UpdateNotificationsDto, UpdateProfileDto, UserResponse } from '../dtos'; import { applyDecorators } from '@nestjs/common'; -import { ApiErrorResponse } from 'src/shared/error'; +import { ApiBadRequest, ApiRequireAuth, ApiValidationError } from 'src/shared/error'; export const GetMeSwagger = () => applyDecorators( @@ -16,15 +23,123 @@ export const GetMeSwagger = () => description: 'Данные профиля успешно получены.', type: UserResponse.Output, }), - ApiErrorResponse(400, 'VALIDATION_FAILED', 'Ошибка во входных данных', [ - { field: 'email', message: 'Некорректный формат почты', code: 'invalid_email' }, + ApiRequireAuth(), + ); + +export const PatchMeSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить данные профиля', + description: 'Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса.', + }), + ApiBody({ type: UpdateProfileDto }), + ApiResponse({ + status: 200, + description: 'Профиль успешно обновлен.', + schema: { + example: { + success: true, + message: 'Профиль успешно обновлен.', + updatedAt: '2026-10-24T14:30:00.000Z', + data: { + fullName: 'Alexey Smirnov', + timezone: 'Europe/Moscow', + }, + }, + }, + }), + ApiValidationError('Ошибка валидации (например, слишком короткое имя)', [ + { + field: 'fullName', + message: 'Строка должна содержать минимум 2 символа', + code: 'too_small', + }, ]), - ApiErrorResponse(401, 'AUTH_REQUIRED', 'Сессия истекла или токен не валиден'), - ApiErrorResponse(403, 'ACCESS_DENIED', 'У вас недостаточно прав для этого действия'), - ApiErrorResponse(404, 'USER_NOT_FOUND', 'Пользователь не найден в базе данных'), - ApiErrorResponse( - 500, - 'INTERNAL_SERVER_ERROR', - 'Произошла критическая ошибка на стороне сервера', - ), + ApiRequireAuth(), + ); + +export const PatchMeNotificationsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить настройки уведомлений', + description: 'Частичное обновление настроек email и push уведомлений.', + }), + ApiBody({ type: UpdateNotificationsDto }), + ApiResponse({ + status: 200, + description: 'Настройки успешно сохранены.', + schema: { + example: { + success: true, + newSettings: { + email: { task_assigned: false }, + }, + }, + }, + }), + ApiValidationError('Некорректный формат настроек'), + ApiRequireAuth(), + ); + +export const GetMeActivitySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить ленту активности пользователя', + description: 'Возвращает список последних действий пользователя (логи).', + }), + ApiQuery({ + name: 'limit', + required: false, + type: String, + description: 'Количество записей для вывода (по умолчанию 10)', + example: '15', + }), + ApiResponse({ + status: 200, + description: 'Список активностей успешно получен.', + schema: { + example: [ + { + id: 'clj1abc230000jk78', + eventType: 'TASK_COMPLETED', + description: 'Завершена задача "Обновить текст лендинга"', + createdAt: '2026-04-10T20:00:00.000Z', + metadata: { taskId: 'clj1xyz990000abc1' }, + }, + ], + }, + }), + ApiRequireAuth(), + ); + +export const PostMeAvatarSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Загрузить новую аватарку', + description: 'Загрузка файла изображения для профиля пользователя.', + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }), + ApiResponse({ + status: 201, + description: 'Аватар успешно загружен.', + schema: { + example: { + avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', + success: true, + }, + }, + }), + ApiBadRequest('Файл не передан или имеет неверный формат'), + ApiRequireAuth(), ); diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..e9b9502 --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +1 @@ +export { ApiBaseController } from './api-controller.decorator'; From eb60b41cf5794e958645b1558e3ca03a25b1b554 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 16:20:59 +0300 Subject: [PATCH 22/47] feat(auth): scaffold auth controller with route stubs --- .../auth/controller/auth.controller.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 221b351..71b1779 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -9,3 +9,57 @@ // POST /auth/2fa/enable — Генерация QR-кода (возвращает otpauth ссылку). // POST /auth/2fa/confirm — Подтверждение включения 2FA (проверка первого кода). // PATCH /auth/2fa/disable — Отключение (обязательно под паролем или кодом). + +import { ApiBaseController } from '../../../shared/decorators'; +import { Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; + +@ApiBaseController('auth', 'Auth') +export class AuthController { + // constructor(private readonly facade: AuthService) {} + + @Post('register') + // @PostRegisterSwagger() + async register() {} + + @Post('login') + // @PostLoginSwagger() + @HttpCode(200) + async login() {} + + @Post('refresh') + // @PostRefreshSwagger() + @HttpCode(200) + async refresh() {} + + @Post('logout') + // @PostLogoutSwagger() + @HttpCode(200) + async logout() {} + + @Get('sessions') + // @GetSessionsSwagger() + async getSessions() {} + + @Delete('sessions/:cuid') + // @DeleteTerminateSessionSwagger + async terminateSession() {} + + @Post('change-password') + // @PostChangePasswordSwagger + @HttpCode(200) + async changePassword() {} + + @Post('2fa/enable') + @HttpCode(200) + // @PostEnable2faSwagger + async enable2fa() {} + + @Patch('2fa/disable') + // @PostDisable2faSwagger + async disable2fa() {} + + @Post('2fa/confirm') + @HttpCode(200) + // @PostConfirm2faSwagger + async confirm2fa() {} +} From c7afb1ad11612f96665124ec28b138ae3dd9de7d Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 16:34:37 +0300 Subject: [PATCH 23/47] feat(auth): scaffold auth service with dependencies --- src/modules/auth/auth.module.ts | 10 +++--- src/modules/auth/auth.service.ts | 33 +++++++++++++++++++ .../auth/controller/auth.controller.ts | 3 +- src/modules/user/user.module.ts | 2 +- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 2815ef3..58526e8 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,10 +1,12 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { UserModule } from '../user'; +import { AuthController } from './controller/auth.controller'; +import { AuthService } from './auth.service'; @Module({ - imports: [forwardRef(() => UserModule)], - controllers: [], - providers: [], + imports: [UserModule], + controllers: [AuthController], + providers: [AuthService], exports: [], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index e69de29..69d3eb3 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { UserService } from '../user/user.service'; + +@Injectable() +export class AuthService { + constructor( + private readonly userService: UserService, + // private readonly jwtService: JwtService, + // @Inject('IRedisService') + // private readonly redisService: IRedisService, + // private readonly emailService: EmailService, + ) {} + + async register() {} + + async login() {} + + async refresh() {} + + async logout() {} + + async getSessions() {} + + async terminateSession() {} + + async changePassword() {} + + async enable2fa() {} + + async disable2fa() {} + + async confirm2fa() {} +} diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 71b1779..1b7231e 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -12,10 +12,11 @@ import { ApiBaseController } from '../../../shared/decorators'; import { Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; +import { AuthService } from '../auth.service'; @ApiBaseController('auth', 'Auth') export class AuthController { - // constructor(private readonly facade: AuthService) {} + constructor(private readonly facade: AuthService) {} @Post('register') // @PostRegisterSwagger() diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 6540e72..f48b90e 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -12,6 +12,6 @@ const REPOSITORY = { imports: [], controllers: [UserController], providers: [REPOSITORY, UserService], - exports: [REPOSITORY], + exports: [UserService], }) export class UserModule {} From 28d12355ea6c510b8e2172190991cd1a4b20836d Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 16:55:24 +0300 Subject: [PATCH 24/47] feat(auth): add zod-based dtos for authentication and 2fa --- src/modules/auth/dtos/2fa.dto.ts | 25 ++++++++++++++++++ src/modules/auth/dtos/auth.dto.ts | 37 +++++++++++++++++++++++++++ src/modules/auth/dtos/index.ts | 3 +++ src/modules/auth/dtos/password.dto.ts | 15 +++++++++++ 4 files changed, 80 insertions(+) create mode 100644 src/modules/auth/dtos/2fa.dto.ts create mode 100644 src/modules/auth/dtos/auth.dto.ts create mode 100644 src/modules/auth/dtos/index.ts create mode 100644 src/modules/auth/dtos/password.dto.ts diff --git a/src/modules/auth/dtos/2fa.dto.ts b/src/modules/auth/dtos/2fa.dto.ts new file mode 100644 index 0000000..8d10068 --- /dev/null +++ b/src/modules/auth/dtos/2fa.dto.ts @@ -0,0 +1,25 @@ +import z from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const Confirm2FaSchema = z + .object({ + code: z + .string() + .length(6, 'Код должен состоять ровно из 6 символов') + .describe('6-значный код из Google Authenticator'), + }) + .describe('Схема подтверждения 2FA'); + +export class Confirm2FaDto extends createZodDto(Confirm2FaSchema) {} + +export const Disable2FaSchema = z + .object({ + password: z.string().optional().describe('Текущий пароль для подтверждения (опционально)'), + code: z.string().optional().describe('Код из приложения (опционально)'), + }) + .refine((data) => data.password || data.code, { + message: 'Нужно передать либо пароль, либо код', + }) + .describe('Схема отключения 2FA'); + +export class Disable2FaDto extends createZodDto(Disable2FaSchema) {} diff --git a/src/modules/auth/dtos/auth.dto.ts b/src/modules/auth/dtos/auth.dto.ts new file mode 100644 index 0000000..2435245 --- /dev/null +++ b/src/modules/auth/dtos/auth.dto.ts @@ -0,0 +1,37 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +export const LoginSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export class LoginDto extends createZodDto(LoginSchema) {} + +export const RegisterSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z + .string() + .min(8, 'Пароль должен содержать минимум 8 символов') + .max(32, 'Пароль должен содержать максимум 32 символа') + .describe('Пароль (минимум 8 символов)'), + fullName: z + .string() + .min(2, 'Имя должно содержать минимум 2 символа') + .max(255) + .describe('Полное имя пользователя'), + }) + .describe('Схема регистрации пользователя'); + +export class RegisterDto extends createZodDto(RegisterSchema) {} + +export const RefreshSchema = z + .object({ + refreshToken: z.string().describe('Refresh токен для обновления сессии'), + }) + .describe('Схема обновления токенов'); + +export class RefreshDto extends createZodDto(RefreshSchema) {} diff --git a/src/modules/auth/dtos/index.ts b/src/modules/auth/dtos/index.ts new file mode 100644 index 0000000..6a0829f --- /dev/null +++ b/src/modules/auth/dtos/index.ts @@ -0,0 +1,3 @@ +export * from './auth.dto'; +export * from './2fa.dto'; +export * from './password.dto'; diff --git a/src/modules/auth/dtos/password.dto.ts b/src/modules/auth/dtos/password.dto.ts new file mode 100644 index 0000000..d1c3643 --- /dev/null +++ b/src/modules/auth/dtos/password.dto.ts @@ -0,0 +1,15 @@ +import { createZodDto } from 'nestjs-zod'; +import z from 'zod/v4'; + +export const ChangePasswordSchema = z + .object({ + oldPassword: z.string().describe('Текущий пароль'), + newPassword: z + .string() + .min(8, 'Новый пароль должен содержать минимум 8 символов') + .max(32, 'Новый пароль должен содержать максимум 32 символа') + .describe('Новый пароль (минимум 8 символов)'), + }) + .describe('Схема смены пароля'); + +export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} From 0f4a5c5334d5062568c4fdb84cad97045076a768 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 17:17:14 +0300 Subject: [PATCH 25/47] refactor(swagger): rename ApiRequireAuth to ApiUnauthorized for better reuse --- src/modules/user/controller/user.swagger.ts | 12 ++++++------ src/shared/error/swagger.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts index 2700ebb..8ca6e4f 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/modules/user/controller/user.swagger.ts @@ -8,7 +8,7 @@ import { } from '@nestjs/swagger'; import { UpdateNotificationsDto, UpdateProfileDto, UserResponse } from '../dtos'; import { applyDecorators } from '@nestjs/common'; -import { ApiBadRequest, ApiRequireAuth, ApiValidationError } from 'src/shared/error'; +import { ApiBadRequest, ApiUnauthorized, ApiValidationError } from 'src/shared/error'; export const GetMeSwagger = () => applyDecorators( @@ -23,7 +23,7 @@ export const GetMeSwagger = () => description: 'Данные профиля успешно получены.', type: UserResponse.Output, }), - ApiRequireAuth(), + ApiUnauthorized(), ); export const PatchMeSwagger = () => @@ -55,7 +55,7 @@ export const PatchMeSwagger = () => code: 'too_small', }, ]), - ApiRequireAuth(), + ApiUnauthorized(), ); export const PatchMeNotificationsSwagger = () => @@ -78,7 +78,7 @@ export const PatchMeNotificationsSwagger = () => }, }), ApiValidationError('Некорректный формат настроек'), - ApiRequireAuth(), + ApiUnauthorized(), ); export const GetMeActivitySwagger = () => @@ -109,7 +109,7 @@ export const GetMeActivitySwagger = () => ], }, }), - ApiRequireAuth(), + ApiUnauthorized(), ); export const PostMeAvatarSwagger = () => @@ -141,5 +141,5 @@ export const PostMeAvatarSwagger = () => }, }), ApiBadRequest('Файл не передан или имеет неверный формат'), - ApiRequireAuth(), + ApiUnauthorized(), ); diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts index 12d57ea..7c4b292 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -33,8 +33,8 @@ export const ApiErrorResponse = ( export const ApiBadRequest = (description: string = 'Некорректный запрос') => applyDecorators(ApiErrorResponse(400, 'BAD_REQUEST', description)); -export const ApiRequireAuth = () => - applyDecorators(ApiErrorResponse(401, 'AUTH_REQUIRED', 'Сессия истекла или токен не валиден')); +export const ApiUnauthorized = (description: string = 'Сессия истекла или токен не валиден') => + applyDecorators(ApiErrorResponse(401, 'AUTH_REQUIRED', description)); export const ApiForbidden = () => applyDecorators( From 037c02457320c222b98e1ed1e8934d8dafae2c7a Mon Sep 17 00:00:00 2001 From: Maxim Date: Sat, 11 Apr 2026 17:35:41 +0300 Subject: [PATCH 26/47] docs(auth): describe auth api endpoints in swagger --- src/modules/auth/auth.module.ts | 2 +- .../auth/controller/auth.controller.ts | 32 ++- src/modules/auth/controller/auth.swagger.ts | 190 ++++++++++++++++++ src/modules/auth/controller/index.ts | 1 + src/shared/error/swagger.ts | 3 + 5 files changed, 217 insertions(+), 11 deletions(-) create mode 100644 src/modules/auth/controller/auth.swagger.ts create mode 100644 src/modules/auth/controller/index.ts diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 58526e8..8d7b5f3 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { UserModule } from '../user'; -import { AuthController } from './controller/auth.controller'; +import { AuthController } from './controller'; import { AuthService } from './auth.service'; @Module({ diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 1b7231e..6d4d647 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -13,54 +13,66 @@ import { ApiBaseController } from '../../../shared/decorators'; import { Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; import { AuthService } from '../auth.service'; +import { + DeleteTerminateSessionSwagger, + GetSessionsSwagger, + PostChangePasswordSwagger, + PostConfirm2faSwagger, + PostDisable2faSwagger, + PostEnable2faSwagger, + PostLoginSwagger, + PostLogoutSwagger, + PostRefreshSwagger, + PostRegisterSwagger, +} from './auth.swagger'; @ApiBaseController('auth', 'Auth') export class AuthController { constructor(private readonly facade: AuthService) {} @Post('register') - // @PostRegisterSwagger() + @PostRegisterSwagger() async register() {} @Post('login') - // @PostLoginSwagger() + @PostLoginSwagger() @HttpCode(200) async login() {} @Post('refresh') - // @PostRefreshSwagger() + @PostRefreshSwagger() @HttpCode(200) async refresh() {} @Post('logout') - // @PostLogoutSwagger() + @PostLogoutSwagger() @HttpCode(200) async logout() {} @Get('sessions') - // @GetSessionsSwagger() + @GetSessionsSwagger() async getSessions() {} @Delete('sessions/:cuid') - // @DeleteTerminateSessionSwagger + @DeleteTerminateSessionSwagger() async terminateSession() {} @Post('change-password') - // @PostChangePasswordSwagger + @PostChangePasswordSwagger() @HttpCode(200) async changePassword() {} @Post('2fa/enable') @HttpCode(200) - // @PostEnable2faSwagger + @PostEnable2faSwagger() async enable2fa() {} @Patch('2fa/disable') - // @PostDisable2faSwagger + @PostDisable2faSwagger() async disable2fa() {} @Post('2fa/confirm') @HttpCode(200) - // @PostConfirm2faSwagger + @PostConfirm2faSwagger() async confirm2fa() {} } diff --git a/src/modules/auth/controller/auth.swagger.ts b/src/modules/auth/controller/auth.swagger.ts new file mode 100644 index 0000000..13abf86 --- /dev/null +++ b/src/modules/auth/controller/auth.swagger.ts @@ -0,0 +1,190 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ApiBadRequest, + ApiConflict, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from 'src/shared/error'; +import { + ChangePasswordDto, + Confirm2FaDto, + Disable2FaDto, + LoginDto, + RefreshDto, + RegisterDto, +} from '../dtos'; + +export const PostRegisterSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Регистрация нового пользователя', + description: 'Создает пользователя, базовые настройки безопасности и уведомлений.', + }), + ApiBody({ type: RegisterDto }), + ApiResponse({ + status: 201, + description: 'Пользователь успешно зарегистрирован.', + schema: { + example: { + success: true, + message: 'Регистрация прошла успешно', + userId: 'clj1abc230000jk78', + }, + }, + }), + ApiValidationError('Ошибка валидации данных (например, неверный формат email)'), + ApiConflict('Пользователь с таким email уже существует'), + ); + +export const PostLoginSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Вход в систему', + description: + 'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.', + }), + ApiBody({ type: LoginDto }), + ApiResponse({ + status: 200, + description: 'Успешный вход.', + schema: { + example: { + success: true, + require2fa: false, + accessToken: 'eyJhbGciOiJIUzI1NiIsInR5c...', + refreshToken: 'def50200508a1768c7e...', + }, + }, + }), + ApiBadRequest('Неверный формат email'), + ApiUnauthorized('Неверный email или пароль'), + ); + +export const PostRefreshSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновление токенов', + description: 'Выдает новую пару Access и Refresh токенов.', + }), + ApiBody({ type: RefreshDto }), + ApiResponse({ + status: 200, + description: 'Токены успешно обновлены.', + schema: { + example: { + success: true, + accessToken: 'eyJhbGciOiJIUzI1NiIsInR5c...', + refreshToken: 'def50200508a1768c7e...', + }, + }, + }), + ApiBadRequest('Ошибка валидации (не передан refresh токен)'), + ApiUnauthorized('Refresh токен недействителен, истек или отозван'), + ); + +export const PostLogoutSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Выход из системы', + description: 'Удаляет текущую сессию пользователя из Redis.', + }), + ApiResponse({ status: 200, description: 'Успешный выход.' }), + ApiUnauthorized(), + ); + +export const GetSessionsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить активные сессии', + description: 'Возвращает список всех активных устройств/сессий пользователя.', + }), + ApiResponse({ + status: 200, + description: 'Список сессий успешно получен.', + schema: { + example: [ + { + id: 'clj1xyz990000abc1', + device: 'Chrome on macOS', + ip: '192.168.1.1', + lastActive: '2026-04-11T14:30:00.000Z', + isCurrent: true, + }, + ], + }, + }), + ApiUnauthorized(), + ); + +export const DeleteTerminateSessionSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Завершить чужую сессию', + description: 'Принудительно удаляет указанную сессию из Redis.', + }), + ApiParam({ name: 'cuid', description: 'ID сессии, которую нужно завершить' }), + ApiResponse({ status: 200, description: 'Сессия успешно завершена.' }), + ApiUnauthorized(), + ApiForbidden(), + ApiNotFound('Сессия не найдена или уже истекла'), + ); + +export const PostChangePasswordSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Смена пароля', + description: 'Требует текущий и новый пароль. Инвалидирует все остальные сессии.', + }), + ApiBody({ type: ChangePasswordDto }), + ApiResponse({ status: 200, description: 'Пароль успешно изменен.' }), + ApiBadRequest('Неверный старый пароль'), + ApiUnauthorized(), + ); + +export const PostEnable2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Генерация QR-кода для 2FA', + description: 'Создает секрет и возвращает ссылку (otpauth) для Google Authenticator.', + }), + ApiResponse({ + status: 200, + description: 'QR-код сгенерирован.', + schema: { + example: { + secret: 'JBSWY3DPEHPK3PXP', + qrCodeUrl: + 'otpauth://totp/TaskTracker:alexey?secret=JBSWY3DPEHPK3PXP&issuer=TaskTracker', + }, + }, + }), + ApiUnauthorized(), + ); + +export const PostDisable2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Подтверждение включения 2FA', + description: 'Проверяет первый код из приложения для окончательной активации 2FA.', + }), + ApiBody({ type: Confirm2FaDto }), + ApiResponse({ status: 200, description: 'Двухфакторная аутентификация успешно включена.' }), + ApiBadRequest('Неверный код подтверждения'), + ApiUnauthorized(), + ); + +export const PostConfirm2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Отключение 2FA', + description: + 'Отключает двухфакторную аутентификацию (требует подтверждения паролем или текущим кодом).', + }), + ApiBody({ type: Disable2FaDto }), + ApiResponse({ status: 200, description: '2FA успешно отключена.' }), + ApiBadRequest('Неверный код или пароль для отключения'), + ApiUnauthorized(), + ); diff --git a/src/modules/auth/controller/index.ts b/src/modules/auth/controller/index.ts new file mode 100644 index 0000000..74c6815 --- /dev/null +++ b/src/modules/auth/controller/index.ts @@ -0,0 +1 @@ +export { AuthController } from './auth.controller'; diff --git a/src/shared/error/swagger.ts b/src/shared/error/swagger.ts index 7c4b292..29def94 100644 --- a/src/shared/error/swagger.ts +++ b/src/shared/error/swagger.ts @@ -48,3 +48,6 @@ export const ApiValidationError = ( description: string = 'Ошибка валидации входных данных', fields: any[] = [], ) => applyDecorators(ApiErrorResponse(400, 'VALIDATION_FAILED', description, fields)); + +export const ApiConflict = (description: string = 'Ресурс уже существует') => + applyDecorators(ApiErrorResponse(409, 'CONFLICT', description)); From f778e5c32f19e1d1cd03e03ece31f69c439ba7b8 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 18:22:55 +0300 Subject: [PATCH 27/47] chore: feat libs per auth flow / rename endpoints --- package.json | 2 + pnpm-lock.yaml | 74 +++++++++++++++++++ .../auth/controller/auth.controller.ts | 26 ++----- 3 files changed, 83 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 318020b..84b24b0 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,10 @@ "@willsoto/nestjs-prometheus": "^6.1.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", + "email-validator": "^2.0.4", "fastify": "^5.8.4", "nestjs-zod": "^5.3.0", + "otplib": "^13.4.0", "pg": "^8.20.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d033cc..64907dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,12 +50,18 @@ importers: drizzle-zod: specifier: ^0.8.3 version: 0.8.3(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0))(zod@4.3.6) + email-validator: + specifier: ^2.0.4 + version: 2.0.4 fastify: specifier: ^5.8.4 version: 5.8.4 nestjs-zod: specifier: ^5.3.0 version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + otplib: + specifier: ^13.4.0 + version: 13.4.0 pg: specifier: ^8.20.0 version: 8.20.0 @@ -1161,6 +1167,24 @@ packages: resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} + '@otplib/core@13.4.0': + resolution: {integrity: sha512-JqOGcvZQi2wIkEQo8f3/iAjstavpXy6gouIDMHygjNuH6Q0FjbHOiXMdcE94RwfgDNMABhzwUmvaPsxvgm9NYw==} + + '@otplib/hotp@13.4.0': + resolution: {integrity: sha512-MJjE0x06mn2ptymz5qZmQveb+vWFuaIftqE0b5/TZZqUOK7l97cV8lRTmid5BpAQMwJDNLW6RnYxGeCRiNdekw==} + + '@otplib/plugin-base32-scure@13.4.0': + resolution: {integrity: sha512-/t9YWJmMbB8bF5z8mXrBZc2FXBe8B/3hG5FhWr9K8cFwFhyxScbPysmZe8s1UTzSA6N+s8Uv8aIfCtVXPNjJWw==} + + '@otplib/plugin-crypto-noble@13.4.0': + resolution: {integrity: sha512-KrvE4m7Zv+TT1944HzgqFJWJpKb6AyoxDbvhPStmBqdMlv5Gekb80d66cuFRL08kkPgJ5gXUSb5SFpYeB+bACg==} + + '@otplib/totp@13.4.0': + resolution: {integrity: sha512-dK+vl0f0ekzf6mCENRI9AKS2NJUC7OjI3+X8e7QSnhQ2WM7I+i4PGpb3QxKi5hxjTtwVuoZwXR2CFtXdcRtNdQ==} + + '@otplib/uri@13.4.0': + resolution: {integrity: sha512-x1ozBa5bPbdZCrrTL/HK21qchiK7jYElTu+0ft22abeEhiLYgH1+SIULvOcVk3CK8YwF4kdcidvkq4ciejucJA==} + '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} @@ -1288,6 +1312,9 @@ packages: '@scarf/scarf@1.4.0': resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + '@simple-libs/child-process-utils@1.0.2': resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} engines: {node: '>=18'} @@ -2136,6 +2163,10 @@ packages: electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} + email-validator@2.0.4: + resolution: {integrity: sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ==} + engines: {node: '>4.0'} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -2949,6 +2980,9 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + otplib@13.4.0: + resolution: {integrity: sha512-RUcYcRMCgRWhUE/XabRppXpUwCwaWBNHe5iPXhdvP8wwDGpGpsIf/kxX/ec3zFsOaM1Oq8lEhUqDwk6W7DHkwg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -4622,6 +4656,33 @@ snapshots: '@opentelemetry/api@1.9.1': {} + '@otplib/core@13.4.0': {} + + '@otplib/hotp@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@otplib/uri': 13.4.0 + + '@otplib/plugin-base32-scure@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@scure/base': 2.0.0 + + '@otplib/plugin-crypto-noble@13.4.0': + dependencies: + '@noble/hashes': 2.0.1 + '@otplib/core': 13.4.0 + + '@otplib/totp@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/uri': 13.4.0 + + '@otplib/uri@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@oxc-project/types@0.124.0': {} '@paralleldrive/cuid2@2.3.1': @@ -4697,6 +4758,8 @@ snapshots: '@scarf/scarf@1.4.0': {} + '@scure/base@2.0.0': {} + '@simple-libs/child-process-utils@1.0.2': dependencies: '@simple-libs/stream-utils': 1.2.0 @@ -5454,6 +5517,8 @@ snapshots: electron-to-chromium@1.5.334: {} + email-validator@2.0.4: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -6313,6 +6378,15 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + otplib@13.4.0: + dependencies: + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/plugin-base32-scure': 13.4.0 + '@otplib/plugin-crypto-noble': 13.4.0 + '@otplib/totp': 13.4.0 + '@otplib/uri': 13.4.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 6d4d647..c5dd560 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -1,15 +1,3 @@ -// POST /auth/register — Регистрация (создание записей в users, user_security, user_notifications). -// POST /auth/login — Вход (выдача Access/Refresh токенов). Если включен 2FA — возврат промежуточного токена. -// POST /auth/refresh — Обновление пары токенов через Refresh Token. -// POST /auth/logout — Удаление текущей сессии из Redis. -// GET /auth/sessions — Список всех активных устройств пользователя. -// DELETE /auth/sessions/:cuid — Принудительное завершение сессии на другом устройстве. -// POST /auth/change-password — Смена пароля (требует oldPassword и newPassword). -// Logic: При успехе обновляем lastPasswordChange и инвалидируем все сессии в Redis, кроме текущей. -// POST /auth/2fa/enable — Генерация QR-кода (возвращает otpauth ссылку). -// POST /auth/2fa/confirm — Подтверждение включения 2FA (проверка первого кода). -// PATCH /auth/2fa/disable — Отключение (обязательно под паролем или кодом). - import { ApiBaseController } from '../../../shared/decorators'; import { Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; import { AuthService } from '../auth.service'; @@ -30,25 +18,25 @@ import { export class AuthController { constructor(private readonly facade: AuthService) {} - @Post('register') + @Post('sign-up') @PostRegisterSwagger() async register() {} - @Post('login') + @Post('sign-in') @PostLoginSwagger() @HttpCode(200) async login() {} + @Post('sign-out') + @PostLogoutSwagger() + @HttpCode(200) + async logout() {} + @Post('refresh') @PostRefreshSwagger() @HttpCode(200) async refresh() {} - @Post('logout') - @PostLogoutSwagger() - @HttpCode(200) - async logout() {} - @Get('sessions') @GetSessionsSwagger() async getSessions() {} From ab77373139984b1fe2214fb94fb310bd081b07f7 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 18:26:06 +0300 Subject: [PATCH 28/47] chore: feat per auth module libs and redis --- package.json | 8 ++ pnpm-lock.yaml | 343 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) diff --git a/package.json b/package.json index 84b24b0..d4a0eeb 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.18", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", "@nestjs/platform-fastify": "^11.1.18", "@nestjs/swagger": "^11.2.7", "@nestjs/throttler": "^6.5.0", @@ -40,8 +42,12 @@ "drizzle-zod": "^0.8.3", "email-validator": "^2.0.4", "fastify": "^5.8.4", + "ioredis": "^5.10.1", "nestjs-zod": "^5.3.0", "otplib": "^13.4.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", "pg": "^8.20.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -54,6 +60,8 @@ "@nestjs/schematics": "^11.0.10", "@nestjs/testing": "^11.1.18", "@types/node": "^20.3.1", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", "@types/pg": "^8.20.0", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64907dc..fa9d0f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,12 @@ importers: '@nestjs/core': specifier: ^11.1.18 version: 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^11.1.18 version: 11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) @@ -56,12 +62,24 @@ importers: fastify: specifier: ^5.8.4 version: 5.8.4 + ioredis: + specifier: ^5.10.1 + version: 5.10.1 nestjs-zod: specifier: ^5.3.0 version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) otplib: specifier: ^13.4.0 version: 13.4.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 pg: specifier: ^8.20.0 version: 8.20.0 @@ -93,6 +111,12 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.19.39 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 '@types/pg': specifier: ^8.20.0 version: 8.20.0 @@ -981,6 +1005,9 @@ packages: '@types/node': optional: true + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1070,6 +1097,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.1.1': resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} peerDependencies: @@ -1083,6 +1115,12 @@ packages: class-validator: optional: true + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/platform-fastify@11.1.18': resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} peerDependencies: @@ -1441,9 +1479,15 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} @@ -1459,21 +1503,60 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-local@1.0.38': + resolution: {integrity: sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/superagent@8.1.9': resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} @@ -1802,6 +1885,9 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1877,6 +1963,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2005,6 +2095,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2160,6 +2254,9 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} @@ -2561,6 +2658,10 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} @@ -2677,6 +2778,16 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2792,6 +2903,30 @@ packages: lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.kebabcase@4.1.1: resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} @@ -2801,6 +2936,9 @@ packages: lodash.mergewith@4.6.2: resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.snakecase@4.1.1: resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} @@ -2999,6 +3137,21 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-local@1.0.0: + resolution: {integrity: sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==} + engines: {node: '>= 0.4.0'} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3025,6 +3178,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + peek-stream@1.1.3: resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} @@ -3176,6 +3332,14 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3350,6 +3514,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -3592,6 +3759,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4473,6 +4644,8 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 + '@ioredis/commands@1.5.1': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4575,11 +4748,22 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 + '@nestjs/passport@11.0.5(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + '@nestjs/platform-fastify@11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@fastify/cors': 11.2.0 @@ -4850,11 +5034,20 @@ snapshots: tslib: 2.8.1 optional: true + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.19.39 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.19.39 + '@types/cookiejar@2.1.5': {} '@types/deep-eql@4.0.2': {} @@ -4871,22 +5064,77 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.39 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 20.19.39 + '@types/methods@1.1.4': {} + '@types/ms@2.1.0': {} + '@types/node@20.19.39': dependencies: undici-types: 6.21.0 + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-local@1.0.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 + '@types/pg@8.20.0': dependencies: '@types/node': 20.19.39 pg-protocol: 1.13.0 pg-types: 2.2.0 + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + '@types/semver@7.7.1': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 20.19.39 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.39 + '@types/superagent@8.1.9': dependencies: '@types/cookiejar': 2.1.5 @@ -5268,6 +5516,8 @@ snapshots: node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5340,6 +5590,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5444,6 +5696,8 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} @@ -5515,6 +5769,10 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + electron-to-chromium@1.5.334: {} email-validator@2.0.4: {} @@ -6031,6 +6289,20 @@ snapshots: ini@4.1.1: {} + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + ipaddr.js@2.3.0: {} is-arrayish@0.2.1: {} @@ -6118,6 +6390,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6214,12 +6510,30 @@ snapshots: lodash.camelcase@4.3.0: {} + lodash.defaults@4.2.0: {} + + lodash.includes@4.3.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.kebabcase@4.1.1: {} lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} + lodash.once@4.1.1: {} + lodash.snakecase@4.1.1: {} lodash.startcase@4.4.0: {} @@ -6406,6 +6720,23 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-local@1.0.0: + dependencies: + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + path-exists@4.0.0: {} path-is-absolute@1.0.1: {} @@ -6423,6 +6754,8 @@ snapshots: pathe@2.0.3: {} + pause@0.0.1: {} + peek-stream@1.1.3: dependencies: buffer-from: 1.1.2 @@ -6578,6 +6911,12 @@ snapshots: real-require@0.2.0: {} + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect-metadata@0.2.2: {} require-directory@2.1.1: {} @@ -6749,6 +7088,8 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} std-env@4.0.0: {} @@ -7000,6 +7341,8 @@ snapshots: util-deprecate@1.0.2: {} + utils-merge@1.0.1: {} + v8-compile-cache-lib@3.0.1: {} vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): From cb834fd86d506aefeabb7becab90ca8f334497b7 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 18:43:13 +0300 Subject: [PATCH 29/47] feat(auth): intergrate with jwt module and update config module --- .env.example | 6 ++++ libs/config/src/config.schema.ts | 15 ++++++++++ .../src/helpers/jwt-secren-validation.ts | 7 +++++ src/modules/auth/auth.module.ts | 28 +++++++++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 libs/config/src/helpers/jwt-secren-validation.ts diff --git a/.env.example b/.env.example index d3f2c1a..aa23d6d 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,9 @@ DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_ REDIS_HOST=redis REDIS_PORT=6379 REDIS_EXTERNAL_PORT=6380 + +JWT_ACCESS_SECRET=same-same-same-same-same +JWT_ACCESS_EXPIRES_IN=15m + +JWT_REFRESH_SECRET=same-same-same-same-same +JWT_REFRESH_EXPIRES_IN=15m \ No newline at end of file diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 348a00f..4dc59db 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -1,4 +1,9 @@ import { z } from 'zod/v4'; +import { jwtSecretValidation } from './helpers/jwt-secren-validation'; + +const timeStringSchema = z.string().regex(/^[0-9]+[smhdw]$/, { + message: 'Invalid time format. Use: s, m, h, d, w (e.g., 15m, 24h, 30d)', +}); export const ConfigSchema = z.object({ PORT: z.coerce.number().default(3000), @@ -29,6 +34,16 @@ export const ConfigSchema = z.object({ .min(1, "CORS_ALLOWED_ORIGINS can't be empty") .transform((val) => val.split(',').map((s) => s.trim())) .pipe(z.array(z.string().url('Each origin must be a valid URL'))), + JWT_ACCESS_SECRET: z.string().refine(jwtSecretValidation, { + message: + 'JWT_ACCESS_SECRET must be at least 32 characters long OR contain at least 5 words separated by hyphens', + }), + JWT_REFRESH_SECRET: z.string().refine(jwtSecretValidation, { + message: + 'JWT_REFRESH_SECRET must be at least 32 characters long OR contain at least 5 words separated by hyphens', + }), + JWT_ACCESS_EXPIRES_IN: timeStringSchema.default('15m'), + JWT_REFRESH_EXPIRES_IN: timeStringSchema.default('30d'), }); export type Config = z.infer; diff --git a/libs/config/src/helpers/jwt-secren-validation.ts b/libs/config/src/helpers/jwt-secren-validation.ts new file mode 100644 index 0000000..27a4e18 --- /dev/null +++ b/libs/config/src/helpers/jwt-secren-validation.ts @@ -0,0 +1,7 @@ +export function jwtSecretValidation(val: string) { + const isLongEnough = val.length >= 32; + const words = val.split('-'); + const hasFiveWords = words.length >= 5 && words.every((word) => word.length > 0); + + return isLongEnough || hasFiveWords; +} diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 8d7b5f3..003ae27 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,10 +1,34 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { UserModule } from '../user'; import { AuthController } from './controller'; import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; @Module({ - imports: [UserModule], + imports: [ + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: async (cfg: ConfigService) => ({ + secret: cfg.get('JWT_ACCESS_SECRET'), + signOptions: { + /** + * Использование 'any' здесь необходимо, так как Zod гарантирует + * формат строки (напр. '15m', '30d') через regex в ConfigSchema, но внутренний тип + * 'StringValue' из библиотеки 'ms' слишком строг для обычного string. + */ + expiresIn: cfg.get('JWT_ACCESS_EXPIRES_IN'), + algorithm: 'HS256', + }, + verifyOptions: { + algorithms: ['HS256'], + ignoreExpiration: false, + clockTolerance: 10, + }, + }), + }), + forwardRef(() => UserModule), + ], controllers: [AuthController], providers: [AuthService], exports: [], From d4c4f9ccf485ea50834091f9f5a7870b79785008 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 19:00:06 +0300 Subject: [PATCH 30/47] feat(redis):chore(infra): integrate redis and setup correct infra --- .env.example | 3 +- infra/compose.dev.yaml | 14 +++ package.json | 1 + pnpm-lock.yaml | 146 +++++++++++++++++++++++++++++++ src/modules/auth/auth.module.ts | 19 ++++ src/modules/auth/auth.service.ts | 6 +- 6 files changed, 185 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index aa23d6d..73279f5 100644 --- a/.env.example +++ b/.env.example @@ -19,8 +19,7 @@ DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_ # --- REDIS --- REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_EXTERNAL_PORT=6380 +REDIS_PORT=7000 JWT_ACCESS_SECRET=same-same-same-same-same JWT_ACCESS_EXPIRES_IN=15m diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml index 7c54f3a..03945d7 100644 --- a/infra/compose.dev.yaml +++ b/infra/compose.dev.yaml @@ -46,6 +46,20 @@ services: retries: 5 profiles: ["infra"] + redis: + hostname: redis + container_name: redis + image: redis:7-alpine + restart: always + ports: + - "7000:6379" + command: redis-server --save 60 1 --loglevel notice + volumes: + - redis_data:/data + networks: + - backend + profiles: ["infra"] + volumes: postgres_data: redis_data: diff --git a/package.json b/package.json index d4a0eeb..fbff566 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.0", + "@nestjs-modules/ioredis": "^2.2.1", "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa9d0f8..a078809 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fastify/static': specifier: ^9.1.0 version: 9.1.0 + '@nestjs-modules/ioredis': + specifier: ^2.2.1 + version: 2.2.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.1.18 version: 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -1047,6 +1050,13 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@nestjs-modules/ioredis@2.2.1': + resolution: {integrity: sha512-wQ08XvlV2s9V+01SKcC5XmFoQ2hMAHP0KuVja8UFZyE/dM0bKI5HSHr+3wQ5ChRpsyhfxF/vKrlPXMlJIr7FIg==} + peerDependencies: + '@nestjs/common': '>=6.7.0' + '@nestjs/core': '>=6.7.0' + ioredis: '>=5.0.0' + '@nestjs/cli@11.0.19': resolution: {integrity: sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==} engines: {node: '>= 20.11'} @@ -1156,6 +1166,54 @@ packages: class-validator: optional: true + '@nestjs/terminus@11.1.1': + resolution: {integrity: sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/microservices': ^10.0.0 || ^11.0.0 + '@nestjs/mongoose': ^11.0.0 + '@nestjs/sequelize': ^10.0.0 || ^11.0.0 + '@nestjs/typeorm': ^10.0.0 || ^11.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + '@nestjs/testing@11.1.18': resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} peerDependencies: @@ -1778,6 +1836,9 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1866,6 +1927,10 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -1909,6 +1974,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001787: resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} @@ -1923,6 +1992,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1931,6 +2004,10 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -3888,6 +3965,10 @@ packages: engines: {node: '>=8'} hasBin: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4688,6 +4769,30 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nestjs-modules/ioredis@2.2.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + ioredis: 5.10.1 + optionalDependencies: + '@nestjs/terminus': 11.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + transitivePeerDependencies: + - '@grpc/grpc-js' + - '@grpc/proto-loader' + - '@mikro-orm/core' + - '@mikro-orm/nestjs' + - '@nestjs/axios' + - '@nestjs/microservices' + - '@nestjs/mongoose' + - '@nestjs/sequelize' + - '@nestjs/typeorm' + - '@prisma/client' + - mongoose + - reflect-metadata + - rxjs + - sequelize + - typeorm + '@nestjs/cli@11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) @@ -4806,6 +4911,16 @@ snapshots: optionalDependencies: '@fastify/static': 9.1.0 + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + optional: true + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -5426,6 +5541,11 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + optional: true + ansi-colors@4.1.3: {} ansi-escapes@7.3.0: @@ -5491,6 +5611,18 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + optional: true + brace-expansion@1.1.13: dependencies: balanced-match: 1.0.2 @@ -5542,6 +5674,9 @@ snapshots: callsites@3.1.0: {} + camelcase@6.3.0: + optional: true + caniuse-lite@1.0.30001787: {} chai@6.2.2: {} @@ -5553,12 +5688,18 @@ snapshots: chardet@2.1.1: {} + check-disk-space@3.4.0: + optional: true + chokidar@4.0.3: dependencies: readdirp: 4.1.2 chrome-trace-event@1.0.4: {} + cli-boxes@2.2.1: + optional: true + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -7446,6 +7587,11 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + optional: true + word-wrap@1.2.5: {} wrap-ansi@6.2.0: diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 003ae27..b60633b 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -4,6 +4,7 @@ import { AuthController } from './controller'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; +import { RedisModule } from '@nestjs-modules/ioredis'; @Module({ imports: [ @@ -27,6 +28,24 @@ import { ConfigService } from '@nestjs/config'; }, }), }), + RedisModule.forRootAsync({ + inject: [ConfigService], + useFactory: async (cfg: ConfigService) => { + const host = cfg.get('REDIS_HOST', { infer: true }); + const port = cfg.get('REDIS_PORT', { infer: true }); + + return { + type: 'single', + url: `redis://${host}:${port}`, + options: { + retryStrategy(times) { + return Math.min(times * 50, 2000); + }, + commandTimeout: 3000, + }, + }; + }, + }), forwardRef(() => UserModule), ], controllers: [AuthController], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 69d3eb3..8ca5d1e 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,13 +1,15 @@ import { Injectable } from '@nestjs/common'; import { UserService } from '../user/user.service'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import Redis from 'ioredis'; @Injectable() export class AuthService { constructor( + @InjectRedis() + private readonly redis: Redis, private readonly userService: UserService, // private readonly jwtService: JwtService, - // @Inject('IRedisService') - // private readonly redisService: IRedisService, // private readonly emailService: EmailService, ) {} From cf6022221d1f8a10416fbc19e78d91ea1b9a4bf6 Mon Sep 17 00:00:00 2001 From: soorq Date: Sat, 11 Apr 2026 22:21:24 +0300 Subject: [PATCH 31/47] feat(auth):chore(ua-parser): integrate logic per register flow #1 --- .env.example | 3 + infra/README.md | 7 + infra/compose.dev.yaml | 16 +- migrations/0001_solid_kronos.sql | 57 +++ migrations/meta/0001_snapshot.json | 369 ++++++++++++++++++ migrations/meta/_journal.json | 31 +- package.json | 3 + pnpm-lock.yaml | 83 ++++ src/modules/auth/auth.module.ts | 9 +- src/modules/auth/auth.service.ts | 35 -- .../auth/controller/auth.controller.ts | 49 ++- src/modules/auth/controller/auth.swagger.ts | 15 +- src/modules/auth/dtos/auth.dto.ts | 43 +- src/modules/auth/helpers/get-device-meta.ts | 30 ++ src/modules/auth/helpers/index.ts | 1 + src/modules/auth/repository/index.ts | 2 + .../session.repository.interface.ts | 13 + .../auth/repository/session.repository.ts | 68 ++++ src/modules/auth/services/auth.service.ts | 171 ++++++++ src/modules/auth/services/index.ts | 2 + src/modules/auth/services/token.service.ts | 52 +++ src/modules/auth/types/index.ts | 1 + src/modules/auth/types/jwt-payload.ts | 8 + src/modules/user/commands/create.command.ts | 29 ++ src/modules/user/commands/find-one.command.ts | 14 + src/modules/user/commands/index.ts | 2 + src/modules/user/entities/user.domain.ts | 2 + src/modules/user/entities/user.entity.ts | 6 +- src/modules/user/index.ts | 1 + .../repository/user.repository.interface.ts | 5 +- .../user/repository/user.repository.ts | 16 +- src/modules/user/user.module.ts | 7 +- src/shared/error/filter.ts | 2 +- 33 files changed, 1057 insertions(+), 95 deletions(-) create mode 100644 infra/README.md create mode 100644 migrations/0001_solid_kronos.sql create mode 100644 migrations/meta/0001_snapshot.json delete mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/helpers/get-device-meta.ts create mode 100644 src/modules/auth/helpers/index.ts create mode 100644 src/modules/auth/repository/index.ts create mode 100644 src/modules/auth/repository/session.repository.interface.ts create mode 100644 src/modules/auth/repository/session.repository.ts create mode 100644 src/modules/auth/services/auth.service.ts create mode 100644 src/modules/auth/services/index.ts create mode 100644 src/modules/auth/services/token.service.ts create mode 100644 src/modules/auth/types/index.ts create mode 100644 src/modules/auth/types/jwt-payload.ts create mode 100644 src/modules/user/commands/create.command.ts create mode 100644 src/modules/user/commands/find-one.command.ts create mode 100644 src/modules/user/commands/index.ts diff --git a/.env.example b/.env.example index 73279f5..bbe8030 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,9 @@ DB_SCHEMA=base DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} # --- REDIS --- +# in the docker network will be +# REDIS_HOST=redis +# at development mode REDIS_HOST=redis REDIS_PORT=7000 diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..310228c --- /dev/null +++ b/infra/README.md @@ -0,0 +1,7 @@ +# Command to run infra at dev mode + +Run it by pwd at root! Not include at this dir + +```sh +docker compose -f .\infra\compose.dev.yaml --env-file .env --profile infra up --build -d -V +``` diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml index 03945d7..a3a6d64 100644 --- a/infra/compose.dev.yaml +++ b/infra/compose.dev.yaml @@ -14,11 +14,9 @@ services: - ../.env ports: - "3000:3000" - depends_on: - database: - condition: service_healthy - redis: - condition: service_started + # depends_on: + # database: + # condition: service_healthy networks: - backend @@ -30,9 +28,9 @@ services: env_file: - ../.env environment: - POSTGRES_USER: ${DB_USERNAME:-admin} - POSTGRES_PASSWORD: ${DB_PASSWORD:-admin} - POSTGRES_DB: ${DB_DATABASE:-tracker} + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} ports: - "6000:5432" volumes: @@ -40,7 +38,7 @@ services: networks: - backend healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}"] + test: ["CMD-SHELL", "pg_isready -q"] interval: 5s timeout: 5s retries: 5 diff --git a/migrations/0001_solid_kronos.sql b/migrations/0001_solid_kronos.sql new file mode 100644 index 0000000..faed36a --- /dev/null +++ b/migrations/0001_solid_kronos.sql @@ -0,0 +1,57 @@ +CREATE TABLE "base"."user_activity" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "event_type" varchar(50) NOT NULL, + "entity_id" varchar, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE "base"."user_notifications" ( + "user_id" text PRIMARY KEY NOT NULL, + "settings" jsonb DEFAULT '{"email":{"task_assigned":true,"mentions":true,"daily_summary":false},"push":{"task_assigned":true,"reminders":true}}'::jsonb NOT NULL +); + +CREATE TABLE "base"."user_security" ( + "user_id" text PRIMARY KEY NOT NULL, + "password_hash" varchar(255) NOT NULL, + "is_2fa_enabled" boolean DEFAULT false NOT NULL, + "two_factor_secret" text, + "last_password_change" timestamp with time zone DEFAULT now() NOT NULL +); + +CREATE TABLE "base"."users" ( + "id" text PRIMARY KEY NOT NULL, + "first_name" varchar(50) NOT NULL, + "last_name" varchar(50) NOT NULL, + "middle_name" varchar(50), + "email" varchar(255) NOT NULL, + "bio" text, + "avatar_url" varchar(512), + "timezone" varchar(50) DEFAULT 'UTC' NOT NULL, + "language" varchar(5) DEFAULT 'ru' NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "users_email_unique" UNIQUE("email") +); + +CREATE TABLE "base"."sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "device_type" varchar(20), + "browser" varchar(50), + "os" varchar(50), + "user_agent" text NOT NULL, + "ip" varchar(45) NOT NULL, + "city" varchar(100), + "country_code" varchar(5), + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "is_revoked" boolean DEFAULT false NOT NULL +); + +ALTER TABLE "base"."user_activity" ADD CONSTRAINT "user_activity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."user_notifications" ADD CONSTRAINT "user_notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."user_security" ADD CONSTRAINT "user_security_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; +ALTER TABLE "base"."sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "base"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/migrations/meta/0001_snapshot.json b/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..3cbcbd8 --- /dev/null +++ b/migrations/meta/0001_snapshot.json @@ -0,0 +1,369 @@ +{ + "id": "c5575cbf-cbee-46d8-af83-95b96a2afceb", + "prevId": "a40dfb7f-7d44-4721-bf37-a197b5f1e479", + "version": "7", + "dialect": "postgresql", + "tables": { + "base.user_activity": { + "name": "user_activity", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_activity_user_id_users_id_fk": { + "name": "user_activity_user_id_users_id_fk", + "tableFrom": "user_activity", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_notifications": { + "name": "user_notifications", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"email\":{\"task_assigned\":true,\"mentions\":true,\"daily_summary\":false},\"push\":{\"task_assigned\":true,\"reminders\":true}}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.user_security": { + "name": "user_security", + "schema": "base", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "is_2fa_enabled": { + "name": "is_2fa_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "two_factor_secret": { + "name": "two_factor_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_password_change": { + "name": "last_password_change", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_security_user_id_users_id_fk": { + "name": "user_security_user_id_users_id_fk", + "tableFrom": "user_security", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.users": { + "name": "users", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "last_name": { + "name": "last_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "middle_name": { + "name": "middle_name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "language": { + "name": "language", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "default": "'ru'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "base.sessions": { + "name": "sessions", + "schema": "base", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false + }, + "browser": { + "name": "browser", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "os": { + "name": "os", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip": { + "name": "ip", + "type": "varchar(45)", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "country_code": { + "name": "country_code", + "type": "varchar(5)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "is_revoked": { + "name": "is_revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "base", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": { + "base": "base" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 17d6d2b..713b19d 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -1,13 +1,20 @@ { - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1775839169154, - "tag": "0000_stale_sunspot", - "breakpoints": true - } - ] -} \ No newline at end of file + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1775839169154, + "tag": "0000_stale_sunspot", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1775925642197, + "tag": "0001_solid_kronos", + "breakpoints": true + } + ] +} diff --git a/package.json b/package.json index fbff566..ab2e714 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@nestjs/throttler": "^6.5.0", "@paralleldrive/cuid2": "^3.3.0", "@willsoto/nestjs-prometheus": "^6.1.0", + "argon2": "^0.44.0", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "email-validator": "^2.0.4", @@ -52,6 +53,7 @@ "pg": "^8.20.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "ua-parser-js": "^2.0.9", "zod": "^4.3.6" }, "devDependencies": { @@ -65,6 +67,7 @@ "@types/passport-local": "^1.0.38", "@types/pg": "^8.20.0", "@types/supertest": "^6.0.0", + "@types/ua-parser-js": "^0.7.39", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitest/coverage-v8": "^4.1.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a078809..3e298d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@willsoto/nestjs-prometheus': specifier: ^6.1.0 version: 6.1.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(prom-client@15.1.3) + argon2: + specifier: ^0.44.0 + version: 0.44.0 drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) @@ -92,6 +95,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + ua-parser-js: + specifier: ^2.0.9 + version: 2.0.9 zod: specifier: ^4.3.6 version: 4.3.6 @@ -126,6 +132,9 @@ importers: '@types/supertest': specifier: ^6.0.0 version: 6.0.3 + '@types/ua-parser-js': + specifier: ^0.7.39 + version: 0.7.39 '@typescript-eslint/eslint-plugin': specifier: ^6.0.0 version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) @@ -343,6 +352,9 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -1291,6 +1303,10 @@ packages: resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} hasBin: true + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1621,6 +1637,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/ua-parser-js@0.7.39': + resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} + '@typescript-eslint/eslint-plugin@6.21.0': resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1870,6 +1889,10 @@ packages: arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argon2@0.44.0: + resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==} + engines: {node: '>=16.17.0'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2145,6 +2168,11 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2184,6 +2212,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-europe-js@0.1.2: + resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2782,6 +2813,9 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-standalone-pwa@0.1.1: + resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -3159,9 +3193,17 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} @@ -3800,6 +3842,13 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-is-frozen@0.1.2: + resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} + + ua-parser-js@2.0.9: + resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} + hasBin: true + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -4250,6 +4299,8 @@ snapshots: tslib: 2.8.1 optional: true + '@epic-web/invariant@1.0.0': {} + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -4994,6 +5045,8 @@ snapshots: bignumber.js: 9.3.1 error-causes: 3.0.2 + '@phc/format@1.0.0': {} + '@pinojs/redact@0.4.0': {} '@pkgr/core@0.2.9': {} @@ -5262,6 +5315,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/ua-parser-js@0.7.39': {} + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5566,6 +5621,13 @@ snapshots: arg@4.1.3: {} + argon2@0.44.0: + dependencies: + '@phc/format': 1.0.0 + cross-env: 10.1.0 + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + argparse@2.0.1: {} array-ify@1.0.0: {} @@ -5817,6 +5879,11 @@ snapshots: create-require@1.1.1: {} + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5843,6 +5910,8 @@ snapshots: dequal@2.0.3: {} + detect-europe-js@0.1.2: {} + detect-libc@2.1.2: {} dezalgo@1.0.4: @@ -6470,6 +6539,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-standalone-pwa@0.1.1: {} + is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -6788,10 +6859,14 @@ snapshots: node-abort-controller@3.1.1: {} + node-addon-api@8.7.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.18.1 + node-gyp-build@4.8.4: {} + node-releases@2.0.37: {} object-inspect@1.13.4: {} @@ -7444,6 +7519,14 @@ snapshots: typescript@5.9.3: {} + ua-is-frozen@0.1.2: {} + + ua-parser-js@2.0.9: + dependencies: + detect-europe-js: 0.1.2 + is-standalone-pwa: 0.1.1 + ua-is-frozen: 0.1.2 + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index b60633b..74910dd 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,10 +1,11 @@ import { Module, forwardRef } from '@nestjs/common'; import { UserModule } from '../user'; import { AuthController } from './controller'; -import { AuthService } from './auth.service'; +import { AuthService, TokenService } from './services'; import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { RedisModule } from '@nestjs-modules/ioredis'; +import { SessionRepository } from './repository'; @Module({ imports: [ @@ -49,7 +50,11 @@ import { RedisModule } from '@nestjs-modules/ioredis'; forwardRef(() => UserModule), ], controllers: [AuthController], - providers: [AuthService], + providers: [ + AuthService, + TokenService, + { provide: 'ISessionRepository', useClass: SessionRepository }, + ], exports: [], }) export class AuthModule {} diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts deleted file mode 100644 index 8ca5d1e..0000000 --- a/src/modules/auth/auth.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { UserService } from '../user/user.service'; -import { InjectRedis } from '@nestjs-modules/ioredis'; -import Redis from 'ioredis'; - -@Injectable() -export class AuthService { - constructor( - @InjectRedis() - private readonly redis: Redis, - private readonly userService: UserService, - // private readonly jwtService: JwtService, - // private readonly emailService: EmailService, - ) {} - - async register() {} - - async login() {} - - async refresh() {} - - async logout() {} - - async getSessions() {} - - async terminateSession() {} - - async changePassword() {} - - async enable2fa() {} - - async disable2fa() {} - - async confirm2fa() {} -} diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index c5dd560..1baa72a 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -1,6 +1,6 @@ import { ApiBaseController } from '../../../shared/decorators'; -import { Delete, Get, HttpCode, Patch, Post } from '@nestjs/common'; -import { AuthService } from '../auth.service'; +import { Body, Delete, Get, HttpCode, Patch, Post, Req, Res } from '@nestjs/common'; +import { AuthService } from '../services/auth.service'; import { DeleteTerminateSessionSwagger, GetSessionsSwagger, @@ -13,6 +13,9 @@ import { PostRefreshSwagger, PostRegisterSwagger, } from './auth.swagger'; +import { SignInDto, SignUpDto, VerifyDto } from '../dtos'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { getDeviceMeta } from '../helpers'; @ApiBaseController('auth', 'Auth') export class AuthController { @@ -20,12 +23,48 @@ export class AuthController { @Post('sign-up') @PostRegisterSwagger() - async register() {} + @HttpCode(202) + async signUp(@Body() dto: SignUpDto) { + console.log('SIGNUP', dto); + return this.facade.signUp(dto); + } + + @Post('verify') + @PostRegisterSwagger() + @HttpCode(201) + async verify( + @Res({ passthrough: true }) res: FastifyReply, + @Req() req: FastifyRequest, + @Body() dto: VerifyDto, + ) { + const meta = getDeviceMeta(req); + const { tokens, ...response } = await this.facade.verify(dto, meta); + res.setCookie('refresh', tokens.refresh, { + httpOnly: true, + secure: false, + path: '/', + sameSite: 'lax', + }); + return { ...response, token: tokens.access }; + } @Post('sign-in') @PostLoginSwagger() - @HttpCode(200) - async login() {} + async signIn( + @Res({ passthrough: true }) res: FastifyReply, + @Req() req: FastifyRequest, + @Body() dto: SignInDto, + ) { + const meta = getDeviceMeta(req); + const { tokens, ...response } = await this.facade.sigIn(dto, meta); + res.setCookie('refresh', tokens.refresh, { + httpOnly: true, + secure: false, + path: '/', + sameSite: 'lax', + }); + return { ...response, token: tokens.access }; + } @Post('sign-out') @PostLogoutSwagger() diff --git a/src/modules/auth/controller/auth.swagger.ts b/src/modules/auth/controller/auth.swagger.ts index 13abf86..5b8d311 100644 --- a/src/modules/auth/controller/auth.swagger.ts +++ b/src/modules/auth/controller/auth.swagger.ts @@ -8,14 +8,7 @@ import { ApiUnauthorized, ApiValidationError, } from 'src/shared/error'; -import { - ChangePasswordDto, - Confirm2FaDto, - Disable2FaDto, - LoginDto, - RefreshDto, - RegisterDto, -} from '../dtos'; +import { ChangePasswordDto, Confirm2FaDto, Disable2FaDto, SignInDto, SignUpDto } from '../dtos'; export const PostRegisterSwagger = () => applyDecorators( @@ -23,7 +16,7 @@ export const PostRegisterSwagger = () => summary: 'Регистрация нового пользователя', description: 'Создает пользователя, базовые настройки безопасности и уведомлений.', }), - ApiBody({ type: RegisterDto }), + ApiBody({ type: SignUpDto.Output }), ApiResponse({ status: 201, description: 'Пользователь успешно зарегистрирован.', @@ -31,7 +24,6 @@ export const PostRegisterSwagger = () => example: { success: true, message: 'Регистрация прошла успешно', - userId: 'clj1abc230000jk78', }, }, }), @@ -46,7 +38,7 @@ export const PostLoginSwagger = () => description: 'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.', }), - ApiBody({ type: LoginDto }), + ApiBody({ type: SignInDto.Output }), ApiResponse({ status: 200, description: 'Успешный вход.', @@ -69,7 +61,6 @@ export const PostRefreshSwagger = () => summary: 'Обновление токенов', description: 'Выдает новую пару Access и Refresh токенов.', }), - ApiBody({ type: RefreshDto }), ApiResponse({ status: 200, description: 'Токены успешно обновлены.', diff --git a/src/modules/auth/dtos/auth.dto.ts b/src/modules/auth/dtos/auth.dto.ts index 2435245..5aeac05 100644 --- a/src/modules/auth/dtos/auth.dto.ts +++ b/src/modules/auth/dtos/auth.dto.ts @@ -1,16 +1,16 @@ import { createZodDto } from 'nestjs-zod'; import { z } from 'zod/v4'; -export const LoginSchema = z +export const SignInSchema = z .object({ email: z.email('Некорректный формат email').describe('Email пользователя'), password: z.string().describe('Пароль пользователя'), }) .describe('Схема входа в систему'); -export class LoginDto extends createZodDto(LoginSchema) {} +export class SignInDto extends createZodDto(SignInSchema) {} -export const RegisterSchema = z +export const SignUpSchema = z .object({ email: z.email('Некорректный формат email').describe('Email пользователя'), password: z @@ -18,20 +18,41 @@ export const RegisterSchema = z .min(8, 'Пароль должен содержать минимум 8 символов') .max(32, 'Пароль должен содержать максимум 32 символа') .describe('Пароль (минимум 8 символов)'), - fullName: z + firstName: z .string() .min(2, 'Имя должно содержать минимум 2 символа') - .max(255) - .describe('Полное имя пользователя'), + .max(50) + .trim() + .describe('Имя'), + lastName: z + .string() + .min(2, 'Фамилия должна содержать минимум 2 символа') + .max(50) + .trim() + .describe('Фамилия'), + middleName: z + .string() + .max(50) + .trim() + .optional() + .or(z.literal('')) + .describe('Отчество (опционально)'), }) .describe('Схема регистрации пользователя'); -export class RegisterDto extends createZodDto(RegisterSchema) {} +export class SignUpDto extends createZodDto(SignUpSchema) {} -export const RefreshSchema = z +export const VerifySchema = z .object({ - refreshToken: z.string().describe('Refresh токен для обновления сессии'), + email: z + .string() + .email('Некорректный формат email') + .describe('Email пользователя, на который был отправлен код'), + code: z + .string() + .length(6, 'Код должен содержать ровно 6 символов') + .describe('6-значный OTP код подтверждения'), }) - .describe('Схема обновления токенов'); + .describe('Схема верификации OTP кода'); -export class RefreshDto extends createZodDto(RefreshSchema) {} +export class VerifyDto extends createZodDto(VerifySchema) {} diff --git a/src/modules/auth/helpers/get-device-meta.ts b/src/modules/auth/helpers/get-device-meta.ts new file mode 100644 index 0000000..b37f69e --- /dev/null +++ b/src/modules/auth/helpers/get-device-meta.ts @@ -0,0 +1,30 @@ +import type { FastifyRequest } from 'fastify'; +import { UAParser } from 'ua-parser-js'; + +export interface DeviceMetadata { + ip: string; + userAgent: string; + browser: string; + os: string; + deviceType: 'mobile' | 'desktop' | 'tablet'; +} + +export function getDeviceMeta(req: FastifyRequest): DeviceMetadata { + const uaString = req.headers['user-agent'] || ''; + const parser = new UAParser(uaString); + const res = parser.getResult(); + + const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0] || req.ip || '0.0.0.0'; + + let deviceType: 'mobile' | 'desktop' | 'tablet' = 'desktop'; + if (res.device.type === 'mobile') deviceType = 'mobile'; + if (res.device.type === 'tablet') deviceType = 'tablet'; + + return { + ip, + userAgent: uaString, + browser: `${res.browser.name || 'Unknown'} ${res.browser.version || ''}`.trim(), + os: `${res.os.name || 'Unknown'} ${res.os.version || ''}`.trim(), + deviceType, + }; +} diff --git a/src/modules/auth/helpers/index.ts b/src/modules/auth/helpers/index.ts new file mode 100644 index 0000000..1740a4d --- /dev/null +++ b/src/modules/auth/helpers/index.ts @@ -0,0 +1 @@ +export { type DeviceMetadata, getDeviceMeta } from './get-device-meta'; diff --git a/src/modules/auth/repository/index.ts b/src/modules/auth/repository/index.ts new file mode 100644 index 0000000..f1ead53 --- /dev/null +++ b/src/modules/auth/repository/index.ts @@ -0,0 +1,2 @@ +export * from './session.repository.interface'; +export { SessionRepository } from './session.repository'; diff --git a/src/modules/auth/repository/session.repository.interface.ts b/src/modules/auth/repository/session.repository.interface.ts new file mode 100644 index 0000000..ede9fc5 --- /dev/null +++ b/src/modules/auth/repository/session.repository.interface.ts @@ -0,0 +1,13 @@ +import { sessions } from '../entities'; + +export type SessionInsert = typeof sessions.$inferInsert; +export type SessionSelect = typeof sessions.$inferSelect; + +export interface ISessionRepository { + create(data: SessionInsert): Promise; + findById(id: string): Promise; + findAllByUserId(userId: string): Promise; + revoke(id: string): Promise; + revokeAllByUserId(userId: string, exceptSessionId?: string): Promise; + deleteExpired(): Promise; +} diff --git a/src/modules/auth/repository/session.repository.ts b/src/modules/auth/repository/session.repository.ts new file mode 100644 index 0000000..be4ba1c --- /dev/null +++ b/src/modules/auth/repository/session.repository.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { eq, and, ne, lt, desc } from 'drizzle-orm'; +import * as schema from '../entities'; +import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; +import { + ISessionRepository, + type SessionInsert, + SessionSelect, +} from './session.repository.interface'; + +@Injectable() +export class SessionRepository implements ISessionRepository { + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService, + ) {} + + async create(data: SessionInsert): Promise { + const [result] = await this.db.insert(schema.sessions).values(data).returning(); + return result; + } + + async findById(id: string): Promise { + const [result] = await this.db + .select() + .from(schema.sessions) + .where(and(eq(schema.sessions.id, id), eq(schema.sessions.isRevoked, false))) + .limit(1); + + return result || null; + } + + async findAllByUserId(userId: string): Promise { + return this.db + .select() + .from(schema.sessions) + .where(and(eq(schema.sessions.userId, userId), eq(schema.sessions.isRevoked, false))) + .orderBy(desc(schema.sessions.createdAt)); + } + + async revoke(id: string): Promise { + await this.db + .update(schema.sessions) + .set({ isRevoked: true, updatedAt: new Date() }) + .where(eq(schema.sessions.id, id)); + } + + async revokeAllByUserId(userId: string, exceptSessionId?: string): Promise { + const filters = [eq(schema.sessions.userId, userId)]; + + if (exceptSessionId) { + filters.push(ne(schema.sessions.id, exceptSessionId)); + } + + await this.db + .update(schema.sessions) + .set({ isRevoked: true, updatedAt: new Date() }) + .where(and(...filters)); + } + + async deleteExpired(): Promise { + const result = await this.db + .delete(schema.sessions) + .where(lt(schema.sessions.expiresAt, new Date())); + + return result.rowCount; + } +} diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts new file mode 100644 index 0000000..42a2142 --- /dev/null +++ b/src/modules/auth/services/auth.service.ts @@ -0,0 +1,171 @@ +import { + BadRequestException, + ConflictException, + Inject, + Injectable, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import Redis from 'ioredis'; +import { SignInDto, SignUpDto, VerifyDto } from '../dtos'; +import { validate } from 'email-validator'; +import { generate, generateSecret, verify as verifyOTP } from 'otplib'; +import * as argon from 'argon2'; +import { CreateUserCommand, FindOneUserCommand } from '../../user'; +import { TokenService } from './token.service'; +import { ISessionRepository } from '../repository'; +import { DeviceMetadata } from '../helpers'; + +@Injectable() +export class AuthService { + constructor( + @InjectRedis() + private readonly redis: Redis, + @Inject('ISessionRepository') + private readonly sessionRepo: ISessionRepository, + private readonly tokenService: TokenService, + private readonly findUserCommand: FindOneUserCommand, + private readonly createUserCommand: CreateUserCommand, + ) {} + + public signUp = async (dto: SignUpDto) => { + const isValidEmail = validate(dto.email); + + if (!isValidEmail) { + throw new UnprocessableEntityException({ + code: 'INVALID_EMAIL_FORMAT', + message: 'Указанный email адрес имеет некорректный формат', + details: { email: dto.email }, + }); + } + + const isExists = await this.findUserCommand.execute(dto.email); + + if (isExists) { + throw new ConflictException({ + code: 'USER_ALREADY_EXISTS', + message: 'Email уже занят другим аккаунтом', + details: { email: dto.email }, + }); + } + + const hashPass = await argon.hash(dto.password); + + const secret = generateSecret(); + const token = await generate({ + secret, + algorithm: 'sha256', + digits: 6, + period: 900, + strategy: 'totp', + }); + + const data = { + user: dto, + password: hashPass, + otp: { token, secret }, + }; + + console.log(data); + + await this.redis.set(`reg:${dto.email}`, JSON.stringify(data), 'EX', 900); + + // this.mailService.sendOtp(dto.email, otp); + + return { + success: true, + message: 'Код подтверждения отправлен на вашу почту', + }; + }; + + public verify = async (dto: VerifyDto, meta: DeviceMetadata) => { + const redisKey = `reg:${dto.email}`; + + const cachedData = await this.redis.get(redisKey); + + if (!cachedData) { + throw new BadRequestException({ + code: 'REGISTRATION_EXPIRED', + message: 'Срок регистрации истек или email не найден. Попробуйте снова.', + }); + } + + const userData = JSON.parse(cachedData); + + const isValid = await verifyOTP({ + token: dto.code, + secret: userData.otp.secret, + algorithm: 'sha256', + digits: 6, + period: 900, + strategy: 'totp', + }); + + if (!isValid) { + throw new BadRequestException({ + code: 'INVALID_OTP', + message: 'Неверный или истекший код подтверждения', + }); + } + + const user = await this.createUserCommand.execute({ + ...userData.user, + password: userData.password, + }); + + const session = await this.sessionRepo.create({ + userId: user.id, + expiresAt: new Date(), + ...meta, + }); + const { access, refresh } = await this.tokenService.generateTokens(user, session.id); + + await this.redis.del(redisKey); + + return { + success: true, + tokens: { access, refresh }, + message: 'Аккаунт успешно подтвержден', + }; + }; + + public sigIn = async (dto: SignInDto, meta: DeviceMetadata) => { + const user = await this.findUserCommand.execute(dto.email); + + if (!user) { + throw new UnauthorizedException({ + code: 'INVALID_CREDENTIALS', + message: 'Неверный email или пароль', + }); + } + + const isPasswordValid = await argon.verify(user.passwordHash, dto.password); + + if (!isPasswordValid) { + throw new UnauthorizedException({ + code: 'INVALID_CREDENTIALS', + message: 'Неверный email или пароль', + }); + } + + const { id } = await this.sessionRepo.create({ + userId: user.id, + expiresAt: new Date(), + ...meta, + }); + + const { access, refresh } = await this.tokenService.generateTokens(user, id); + + return { + success: true, + tokens: { + access, + refresh, + }, + message: 'Вы успешно вошли в систему', + }; + }; + + public refresh = async () => {}; +} diff --git a/src/modules/auth/services/index.ts b/src/modules/auth/services/index.ts new file mode 100644 index 0000000..f39bab2 --- /dev/null +++ b/src/modules/auth/services/index.ts @@ -0,0 +1,2 @@ +export { AuthService } from './auth.service'; +export { TokenService } from './token.service'; diff --git a/src/modules/auth/services/token.service.ts b/src/modules/auth/services/token.service.ts new file mode 100644 index 0000000..b61426c --- /dev/null +++ b/src/modules/auth/services/token.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { JwtPayload } from '../types'; + +@Injectable() +export class TokenService { + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + async generateTokens(user: any, sessionId: string) { + const domain = this.configService.get('DOMAIN'); + + const payload = { + jti: sessionId, + sub: user.id, + email: user.email, + iss: btoa(domain), + // TODO: ADD TO ENV GLOBAL + aud: btoa('task-tracker-client'), + role: user.role, + }; + + const [access, refresh] = await Promise.all([ + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_ACCESS_SECRET'), + expiresIn: this.configService.get('JWT_ACCESS_EXPIRES_IN'), + }), + this.jwtService.signAsync(payload, { + secret: this.configService.get('JWT_REFRESH_SECRET'), + expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN'), + }), + ]); + + return { access, refresh }; + } + + async validateToken(token: string, type: 'access' | 'refresh'): Promise { + try { + const secret = + type === 'access' + ? this.configService.get('JWT_ACCESS_SECRET') + : this.configService.get('JWT_REFRESH_SECRET'); + + return this.jwtService.verifyAsync(token, { secret }); + } catch (e) { + return null; + } + } +} diff --git a/src/modules/auth/types/index.ts b/src/modules/auth/types/index.ts new file mode 100644 index 0000000..324f5b4 --- /dev/null +++ b/src/modules/auth/types/index.ts @@ -0,0 +1 @@ +export * from './jwt-payload'; diff --git a/src/modules/auth/types/jwt-payload.ts b/src/modules/auth/types/jwt-payload.ts new file mode 100644 index 0000000..c788698 --- /dev/null +++ b/src/modules/auth/types/jwt-payload.ts @@ -0,0 +1,8 @@ +export interface JwtPayload { + sub: string; + email: string; + role: string; + iss: string; + aud: string; + jti: string; +} diff --git a/src/modules/user/commands/create.command.ts b/src/modules/user/commands/create.command.ts new file mode 100644 index 0000000..b5e1d54 --- /dev/null +++ b/src/modules/user/commands/create.command.ts @@ -0,0 +1,29 @@ +import { ConflictException, Inject, Injectable } from '@nestjs/common'; +import { IUserRepository } from '../repository/user.repository.interface'; +import { NewUser } from '../entities/user.domain'; +import { createId } from '@paralleldrive/cuid2'; + +@Injectable() +export class CreateUserCommand { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(dto: NewUser & { password: string }) { + const existingUser = await this.repository.findByEmail(dto.email); + + if (existingUser) { + throw new ConflictException(`User with email ${dto.email} already exists`); + } + + const user = await this.repository.create(dto); + await this.repository.logActivity({ + eventType: 'registered', + userId: user.id, + id: createId(), + }); + await this.repository.updatePasswordHash(user.id, dto.password); + return user; + } +} diff --git a/src/modules/user/commands/find-one.command.ts b/src/modules/user/commands/find-one.command.ts new file mode 100644 index 0000000..1e815f9 --- /dev/null +++ b/src/modules/user/commands/find-one.command.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IUserRepository } from '../repository/user.repository.interface'; + +@Injectable() +export class FindOneUserCommand { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(email: string) { + return await this.repository.findByEmail(email); + } +} diff --git a/src/modules/user/commands/index.ts b/src/modules/user/commands/index.ts new file mode 100644 index 0000000..0400d0a --- /dev/null +++ b/src/modules/user/commands/index.ts @@ -0,0 +1,2 @@ +export { CreateUserCommand } from './create.command'; +export { FindOneUserCommand } from './find-one.command'; diff --git a/src/modules/user/entities/user.domain.ts b/src/modules/user/entities/user.domain.ts index ffd0966..edb7e34 100644 --- a/src/modules/user/entities/user.domain.ts +++ b/src/modules/user/entities/user.domain.ts @@ -17,3 +17,5 @@ export type UserProfile = User & { security: Omit; notifications: UserNotifications['settings']; }; + +export type UserWithPassword = User & UserSecurity; diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts index 3f4955b..9d06268 100644 --- a/src/modules/user/entities/user.entity.ts +++ b/src/modules/user/entities/user.entity.ts @@ -6,7 +6,11 @@ export const users = baseSchema.table('users', { id: text('id') .primaryKey() .$defaultFn(() => createId()), - fullName: varchar('full_name', { length: 255 }).notNull(), + + firstName: varchar('first_name', { length: 50 }).notNull(), + lastName: varchar('last_name', { length: 50 }).notNull(), + middleName: varchar('middle_name', { length: 50 }), + email: varchar('email', { length: 255 }).notNull().unique(), bio: text('bio'), avatarUrl: varchar('avatar_url', { length: 512 }), diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts index 2266068..009182f 100644 --- a/src/modules/user/index.ts +++ b/src/modules/user/index.ts @@ -1,2 +1,3 @@ export { UserModule } from './user.module'; export { UserRepository } from './repository/user.repository'; +export { CreateUserCommand, FindOneUserCommand } from './commands'; diff --git a/src/modules/user/repository/user.repository.interface.ts b/src/modules/user/repository/user.repository.interface.ts index 5121fd0..661e25a 100644 --- a/src/modules/user/repository/user.repository.interface.ts +++ b/src/modules/user/repository/user.repository.interface.ts @@ -1,15 +1,16 @@ -import { +import type { NewUser, NewUserActivity, User, UserActivity, UserNotifications, UserProfile, + UserWithPassword, } from '../entities/user.domain'; export interface IUserRepository { findById(id: string): Promise; - findByEmail(email: string): Promise; + findByEmail(email: string): Promise; existsByEmail(email: string): Promise; diff --git a/src/modules/user/repository/user.repository.ts b/src/modules/user/repository/user.repository.ts index 9bd0ab0..4184aa1 100644 --- a/src/modules/user/repository/user.repository.ts +++ b/src/modules/user/repository/user.repository.ts @@ -2,7 +2,13 @@ import * as sc from '../entities'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { IUserRepository } from './user.repository.interface'; import { Inject, Injectable } from '@nestjs/common'; -import { NewUser, NewUserActivity, User, UserNotifications } from '../entities/user.domain'; +import type { + NewUser, + NewUserActivity, + User, + UserNotifications, + UserWithPassword, +} from '../entities/user.domain'; import { createId } from '@paralleldrive/cuid2'; import { desc, eq, count } from 'drizzle-orm'; @@ -48,13 +54,17 @@ export class UserRepository implements IUserRepository { return result || null; } - async findByEmail(email: string): Promise { + async findByEmail(email: string): Promise { const [result] = await this.repository .select() .from(sc.users) + .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) .where(eq(sc.users.email, email)) .limit(1); - return result || null; + + const resulted = { ...result.users, ...result.user_security }; + + return resulted || null; } async findSecurityByUserId(userId: string) { diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index f48b90e..459ee82 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -2,16 +2,19 @@ import { Module } from '@nestjs/common'; import { UserController } from './controller'; import { UserService } from './user.service'; import { UserRepository } from './repository/user.repository'; +import { CreateUserCommand, FindOneUserCommand } from './commands'; const REPOSITORY = { provide: 'IUserRepository', useClass: UserRepository, }; +const COMMANDS = [CreateUserCommand, FindOneUserCommand]; + @Module({ imports: [], controllers: [UserController], - providers: [REPOSITORY, UserService], - exports: [UserService], + providers: [...COMMANDS, REPOSITORY, UserService], + exports: [...COMMANDS], }) export class UserModule {} diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts index 4cbf29b..d571387 100644 --- a/src/shared/error/filter.ts +++ b/src/shared/error/filter.ts @@ -45,6 +45,6 @@ export class GlobalExceptionFilter implements ExceptionFilter { }, }; - response.status(status).json(errorResponse); + response.status(status).send(errorResponse); } } From 759a6f9318ae7a80f2f042e97e448ce75f9c10b2 Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 00:24:40 +0300 Subject: [PATCH 32/47] feat(auth): integrate logic per sign flow #2 --- src/modules/auth/auth.module.ts | 3 + .../auth/controller/auth.controller.ts | 35 +++++++-- src/modules/auth/services/auth.service.ts | 73 ++++++++++++++++++- .../auth/strategies/bearer.strategy.ts | 21 ++++++ .../auth/strategies/cookie.strategy.ts | 32 ++++++++ src/modules/auth/strategies/index.ts | 2 + src/modules/user/commands/find-one.command.ts | 17 ++++- .../user/controller/user.controller.ts | 4 +- .../user/repository/user.repository.ts | 24 +++--- src/shared/guards/bearer.guard.ts | 5 ++ src/shared/guards/cookie.guard.ts | 5 ++ src/shared/guards/index.ts | 2 + 12 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 src/modules/auth/strategies/bearer.strategy.ts create mode 100644 src/modules/auth/strategies/cookie.strategy.ts create mode 100644 src/modules/auth/strategies/index.ts create mode 100644 src/shared/guards/bearer.guard.ts create mode 100644 src/shared/guards/cookie.guard.ts create mode 100644 src/shared/guards/index.ts diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 74910dd..970961b 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -6,6 +6,7 @@ import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { RedisModule } from '@nestjs-modules/ioredis'; import { SessionRepository } from './repository'; +import { BearerStrategy, CookieStrategy } from './strategies'; @Module({ imports: [ @@ -53,6 +54,8 @@ import { SessionRepository } from './repository'; providers: [ AuthService, TokenService, + CookieStrategy, + BearerStrategy, { provide: 'ISessionRepository', useClass: SessionRepository }, ], exports: [], diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 1baa72a..1344ba7 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -1,5 +1,5 @@ import { ApiBaseController } from '../../../shared/decorators'; -import { Body, Delete, Get, HttpCode, Patch, Post, Req, Res } from '@nestjs/common'; +import { Body, Delete, Get, HttpCode, Patch, Post, Req, Res, UseGuards } from '@nestjs/common'; import { AuthService } from '../services/auth.service'; import { DeleteTerminateSessionSwagger, @@ -16,6 +16,7 @@ import { import { SignInDto, SignUpDto, VerifyDto } from '../dtos'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { getDeviceMeta } from '../helpers'; +import { BearerAuthGuard, CookieAuthGuard } from 'src/shared/guards'; @ApiBaseController('auth', 'Auth') export class AuthController { @@ -25,7 +26,6 @@ export class AuthController { @PostRegisterSwagger() @HttpCode(202) async signUp(@Body() dto: SignUpDto) { - console.log('SIGNUP', dto); return this.facade.signUp(dto); } @@ -39,12 +39,14 @@ export class AuthController { ) { const meta = getDeviceMeta(req); const { tokens, ...response } = await this.facade.verify(dto, meta); + res.setCookie('refresh', tokens.refresh, { httpOnly: true, secure: false, path: '/', sameSite: 'lax', }); + return { ...response, token: tokens.access }; } @@ -57,24 +59,47 @@ export class AuthController { ) { const meta = getDeviceMeta(req); const { tokens, ...response } = await this.facade.sigIn(dto, meta); + res.setCookie('refresh', tokens.refresh, { httpOnly: true, secure: false, path: '/', sameSite: 'lax', }); + return { ...response, token: tokens.access }; } @Post('sign-out') + @UseGuards(BearerAuthGuard) @PostLogoutSwagger() - @HttpCode(200) - async logout() {} + async logout(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { + const session = req.cookies['refresh']; + const response = await this.facade.signOut(session); + + res.clearCookie('refresh', { path: '/' }); + + return response; + } @Post('refresh') + @UseGuards(CookieAuthGuard) @PostRefreshSwagger() @HttpCode(200) - async refresh() {} + async refresh(@Res({ passthrough: true }) res: FastifyReply, @Req() req: FastifyRequest) { + const meta = getDeviceMeta(req); + const session = req.cookies['refresh']; + const { tokens, ...response } = await this.facade.refresh(session, meta); + + res.setCookie('refresh', tokens.refresh, { + httpOnly: true, + secure: false, + path: '/', + sameSite: 'lax', + }); + + return { token: tokens.access, ...response }; + } @Get('sessions') @GetSessionsSwagger() diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 42a2142..4ef6471 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -40,7 +40,7 @@ export class AuthService { }); } - const isExists = await this.findUserCommand.execute(dto.email); + const isExists = await this.findUserCommand.execute({ email: dto.email }); if (isExists) { throw new ConflictException({ @@ -131,7 +131,7 @@ export class AuthService { }; public sigIn = async (dto: SignInDto, meta: DeviceMetadata) => { - const user = await this.findUserCommand.execute(dto.email); + const user = await this.findUserCommand.execute({ email: dto.email }); if (!user) { throw new UnauthorizedException({ @@ -167,5 +167,72 @@ export class AuthService { }; }; - public refresh = async () => {}; + public refresh = async (token: string, metadata: DeviceMetadata) => { + const payload = await this.tokenService.validateToken(token, 'refresh'); + + if (!payload || !payload.jti) { + throw new UnauthorizedException({ + code: 'INVALID_TOKEN', + message: 'Сессия недействительна или истекла', + }); + } + + const session = await this.sessionRepo.findById(payload.jti); + + if (!session || session.isRevoked) { + throw new UnauthorizedException({ + code: 'SESSION_REVOKED', + message: 'Ваша сессия была отозвана или завершена', + }); + } + + console.log(session); + + const user = await this.findUserCommand.execute({ id: session.userId }); + + if (!user) { + await this.sessionRepo.revoke(session.id); + throw new UnauthorizedException({ + code: 'USER_NOT_FOUND', + message: 'Аккаунт пользователя не найден', + }); + } + + await this.sessionRepo.revoke(session.id); + + const newSession = await this.sessionRepo.create({ + userId: user.id, + ...metadata, + expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), + }); + + const { access, refresh } = await this.tokenService.generateTokens(user, newSession.id); + + return { + tokens: { access, refresh }, + success: true, + message: 'Токены успешно обновлены', + }; + }; + + public signOut = async (token: string) => { + const payload = await this.tokenService.validateToken(token, 'refresh'); + + if (!payload?.jti) { + throw new UnauthorizedException({ code: 'SESSION_EXPIRED', message: 'Сессия истекла' }); + } + + const session = await this.sessionRepo.findById(payload.jti); + + if (!session) { + throw new UnauthorizedException({ + code: 'SESSION_NOT_FOUND', + message: 'Сессия не найдена', + }); + } + + await this.sessionRepo.revoke(session.id); + + return { success: true, message: 'Успешно вышли из системы!' }; + }; } diff --git a/src/modules/auth/strategies/bearer.strategy.ts b/src/modules/auth/strategies/bearer.strategy.ts new file mode 100644 index 0000000..d7914ed --- /dev/null +++ b/src/modules/auth/strategies/bearer.strategy.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { JwtPayload } from '../types'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy, ExtractJwt } from 'passport-jwt'; + +@Injectable() +export class BearerStrategy extends PassportStrategy(Strategy, 'bearer') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: configService.get('JWT_ACCESS_SECRET'), + issuer: configService.get('JWT_ISSUER'), + audience: configService.get('JWT_AUDIENCE'), + }); + } + + validate(payload: JwtPayload) { + return payload; + } +} diff --git a/src/modules/auth/strategies/cookie.strategy.ts b/src/modules/auth/strategies/cookie.strategy.ts new file mode 100644 index 0000000..d821a1f --- /dev/null +++ b/src/modules/auth/strategies/cookie.strategy.ts @@ -0,0 +1,32 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import type { FastifyRequest } from 'fastify'; +import type { JwtPayload } from '../types'; + +@Injectable() +export class CookieStrategy extends PassportStrategy(Strategy, 'cookie') { + constructor(configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromExtractors([ + (request: FastifyRequest) => { + return request?.cookies?.['refresh']; + }, + ]), + secretOrKey: configService.get('JWT_REFRESH_SECRET'), + passReqToCallback: true, + }); + } + + validate(_req: FastifyRequest, payload: JwtPayload) { + if (!payload || !payload.jti) { + throw new UnauthorizedException({ + code: 'INVALID_REFRESH_TOKEN', + message: 'Refresh токен невалиден или протух', + }); + } + + return payload; + } +} diff --git a/src/modules/auth/strategies/index.ts b/src/modules/auth/strategies/index.ts new file mode 100644 index 0000000..4ea10ce --- /dev/null +++ b/src/modules/auth/strategies/index.ts @@ -0,0 +1,2 @@ +export { BearerStrategy } from './bearer.strategy'; +export { CookieStrategy } from './cookie.strategy'; diff --git a/src/modules/user/commands/find-one.command.ts b/src/modules/user/commands/find-one.command.ts index 1e815f9..efa6682 100644 --- a/src/modules/user/commands/find-one.command.ts +++ b/src/modules/user/commands/find-one.command.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IUserRepository } from '../repository/user.repository.interface'; +import { User, UserWithPassword } from '../entities/user.domain'; @Injectable() export class FindOneUserCommand { @@ -8,7 +9,19 @@ export class FindOneUserCommand { private readonly repository: IUserRepository, ) {} - async execute(email: string) { - return await this.repository.findByEmail(email); + async execute(params: { email: string }): Promise; + async execute(params: { id: string }): Promise; + async execute(params: { email?: string; id?: string }): Promise { + const { email, id } = params; + + if (email) { + return this.repository.findByEmail(email); + } + + if (id) { + return this.repository.findById(id); + } + + throw new Error('FindOneUserCommand: email or id must be provided'); } } diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts index fb84dd5..cfa6fb6 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/modules/user/controller/user.controller.ts @@ -1,4 +1,4 @@ -import { Body, Get, Patch, Post, Query } from '@nestjs/common'; +import { Body, Get, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { UserService } from '../user.service'; import { createId } from '@paralleldrive/cuid2'; import { @@ -10,8 +10,10 @@ import { } from './user.swagger'; import { UpdateNotificationsDto, UpdateProfileDto } from '../dtos'; import { ApiBaseController } from '../../../shared/decorators'; +import { BearerAuthGuard } from 'src/shared/guards'; @ApiBaseController('users', 'Users') +@UseGuards(BearerAuthGuard) export class UserController { constructor(private readonly facade: UserService) {} diff --git a/src/modules/user/repository/user.repository.ts b/src/modules/user/repository/user.repository.ts index 4184aa1..d3c5595 100644 --- a/src/modules/user/repository/user.repository.ts +++ b/src/modules/user/repository/user.repository.ts @@ -25,8 +25,7 @@ export class UserRepository implements IUserRepository { .from(sc.users) .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)) - .where(eq(sc.users.id, id)) - .limit(1); + .where(eq(sc.users.id, id)); if (rows.length === 0) return null; @@ -46,11 +45,7 @@ export class UserRepository implements IUserRepository { } async findById(id: string): Promise { - const [result] = await this.repository - .select() - .from(sc.users) - .where(eq(sc.users.id, id)) - .limit(1); + const [result] = await this.repository.select().from(sc.users).where(eq(sc.users.id, id)); return result || null; } @@ -59,20 +54,23 @@ export class UserRepository implements IUserRepository { .select() .from(sc.users) .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) - .where(eq(sc.users.email, email)) - .limit(1); + .where(eq(sc.users.email, email)); - const resulted = { ...result.users, ...result.user_security }; + if (!result || !result.users) { + return null; + } - return resulted || null; + return { + ...result.users, + ...result.user_security, + }; } async findSecurityByUserId(userId: string) { const [result] = await this.repository .select() .from(sc.userSecurity) - .where(eq(sc.userSecurity.userId, userId)) - .limit(1); + .where(eq(sc.userSecurity.userId, userId)); return result || null; } diff --git a/src/shared/guards/bearer.guard.ts b/src/shared/guards/bearer.guard.ts new file mode 100644 index 0000000..65f33b7 --- /dev/null +++ b/src/shared/guards/bearer.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class BearerAuthGuard extends AuthGuard('bearer') {} diff --git a/src/shared/guards/cookie.guard.ts b/src/shared/guards/cookie.guard.ts new file mode 100644 index 0000000..9ae8936 --- /dev/null +++ b/src/shared/guards/cookie.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class CookieAuthGuard extends AuthGuard('cookie') {} diff --git a/src/shared/guards/index.ts b/src/shared/guards/index.ts new file mode 100644 index 0000000..20ada34 --- /dev/null +++ b/src/shared/guards/index.ts @@ -0,0 +1,2 @@ +export { BearerAuthGuard } from './bearer.guard'; +export { CookieAuthGuard } from './cookie.guard'; From 4174a2c7303e736013528d09fadf79efe98d1339 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 00:05:01 +0300 Subject: [PATCH 33/47] feat(mail): add mail module and Handlebars templates --- .env.example | 12 +++++- libs/config/src/config.schema.ts | 6 +++ package.json | 3 ++ pnpm-lock.yaml | 49 ++++++++++++++++++++++++ src/modules/app/app.module.ts | 2 + src/modules/mail/index.ts | 1 + src/modules/mail/mail.module.ts | 9 +++++ src/modules/mail/mail.service.ts | 66 ++++++++++++++++++++++++++++++++ templates/confirmation.hbs | 57 +++++++++++++++++++++++++++ templates/reset-password.hbs | 56 +++++++++++++++++++++++++++ 10 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 src/modules/mail/index.ts create mode 100644 src/modules/mail/mail.module.ts create mode 100644 src/modules/mail/mail.service.ts create mode 100644 templates/confirmation.hbs create mode 100644 templates/reset-password.hbs diff --git a/.env.example b/.env.example index bbe8030..a59f112 100644 --- a/.env.example +++ b/.env.example @@ -28,4 +28,14 @@ JWT_ACCESS_SECRET=same-same-same-same-same JWT_ACCESS_EXPIRES_IN=15m JWT_REFRESH_SECRET=same-same-same-same-same -JWT_REFRESH_EXPIRES_IN=15m \ No newline at end of file +JWT_REFRESH_EXPIRES_IN=15m + +# --- MAIL SETTINGS --- +MAIL_HOST=smtp.gmail.com +MAIL_PORT=465 +MAIL_USER=example@gmail.com + +# 16x password +MAIL_PASSWORD=xxxxxxxxyyyyyyyy +MAIL_FROM_NAME="Task Tracker" +MAIL_FROM_EMAIL=example@gmail.com \ No newline at end of file diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 4dc59db..43ac0c3 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -44,6 +44,12 @@ export const ConfigSchema = z.object({ }), JWT_ACCESS_EXPIRES_IN: timeStringSchema.default('15m'), JWT_REFRESH_EXPIRES_IN: timeStringSchema.default('30d'), + MAIL_HOST: z.string().default('smtp.gmail.com'), + MAIL_PORT: z.coerce.number().default(465), + MAIL_USER: z.email('MAIL_USER must be a valid email'), + MAIL_PASSWORD: z.string().min(1, 'MAIL_PASSWORD is missing'), + MAIL_FROM_NAME: z.string().default('Foodies App'), + MAIL_FROM_EMAIL: z.email().optional(), }); export type Config = z.infer; diff --git a/package.json b/package.json index ab2e714..4849ac0 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,10 @@ "drizzle-zod": "^0.8.3", "email-validator": "^2.0.4", "fastify": "^5.8.4", + "handlebars": "^4.7.9", "ioredis": "^5.10.1", "nestjs-zod": "^5.3.0", + "nodemailer": "^8.0.5", "otplib": "^13.4.0", "passport": "^0.7.0", "passport-jwt": "^4.0.1", @@ -63,6 +65,7 @@ "@nestjs/schematics": "^11.0.10", "@nestjs/testing": "^11.1.18", "@types/node": "^20.3.1", + "@types/nodemailer": "^8.0.0", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", "@types/pg": "^8.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e298d5..8e91630 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,12 +68,18 @@ importers: fastify: specifier: ^5.8.4 version: 5.8.4 + handlebars: + specifier: ^4.7.9 + version: 4.7.9 ioredis: specifier: ^5.10.1 version: 5.10.1 nestjs-zod: specifier: ^5.3.0 version: 5.3.0(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + nodemailer: + specifier: ^8.0.5 + version: 8.0.5 otplib: specifier: ^13.4.0 version: 13.4.0 @@ -120,6 +126,9 @@ importers: '@types/node': specifier: ^20.3.1 version: 20.19.39 + '@types/nodemailer': + specifier: ^8.0.0 + version: 8.0.0 '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 @@ -1601,6 +1610,9 @@ packages: '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} + '@types/nodemailer@8.0.0': + resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==} + '@types/passport-jwt@4.0.1': resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} @@ -2705,6 +2717,11 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -3207,6 +3224,10 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + nodemailer@8.0.5: + resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} + engines: {node: '>=6.0.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -3849,6 +3870,11 @@ packages: resolution: {integrity: sha512-OsqGhxyo/wGdLSXMSJxuMGN6H4gDnKz6Fb3IBm4bxZFMnyy0sdf6MN96Ie8tC6z/btdO+Bsy8guxlvLdwT076w==} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + uid@2.0.2: resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} engines: {node: '>=8'} @@ -4022,6 +4048,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -5262,6 +5291,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/nodemailer@8.0.0': + dependencies: + '@types/node': 20.19.39 + '@types/passport-jwt@4.0.1': dependencies: '@types/jsonwebtoken': 9.0.10 @@ -6449,6 +6482,15 @@ snapshots: graphemer@1.4.0: {} + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -6869,6 +6911,8 @@ snapshots: node-releases@2.0.37: {} + nodemailer@8.0.5: {} + object-inspect@1.13.4: {} obug@2.1.1: {} @@ -7527,6 +7571,9 @@ snapshots: is-standalone-pwa: 0.1.1 ua-is-frozen: 0.1.2 + uglify-js@3.19.3: + optional: true + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -7677,6 +7724,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index dc85c16..a9eea71 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -11,6 +11,7 @@ import { HealthModule } from '@libs/health'; import { UserModule } from '../user'; import { GlobalExceptionFilter } from 'src/shared/error'; import { AuthModule } from '../auth'; +import { MailModule } from '../mail'; @Module({ imports: [ @@ -36,6 +37,7 @@ import { AuthModule } from '../auth'; }), AuthModule, UserModule, + MailModule, HealthModule.register('gateway'), ], controllers: [AppController], diff --git a/src/modules/mail/index.ts b/src/modules/mail/index.ts new file mode 100644 index 0000000..5d54413 --- /dev/null +++ b/src/modules/mail/index.ts @@ -0,0 +1 @@ +export { MailModule } from './mail.module'; diff --git a/src/modules/mail/mail.module.ts b/src/modules/mail/mail.module.ts new file mode 100644 index 0000000..09f6249 --- /dev/null +++ b/src/modules/mail/mail.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { MailService } from './mail.service'; + +@Global() +@Module({ + providers: [MailService], + exports: [MailService], +}) +export class MailModule {} diff --git a/src/modules/mail/mail.service.ts b/src/modules/mail/mail.service.ts new file mode 100644 index 0000000..5383dd8 --- /dev/null +++ b/src/modules/mail/mail.service.ts @@ -0,0 +1,66 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; +import * as hbs from 'handlebars'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as validator from 'email-validator'; +import { BadRequestException } from '@nestjs/common'; + +@Injectable() +export class MailService { + private transporter: nodemailer.Transporter; + + constructor(private cfg: ConfigService) { + this.transporter = nodemailer.createTransport({ + host: this.cfg.get('MAIL_HOST'), + port: this.cfg.get('MAIL_PORT'), + secure: true, + auth: { + user: this.cfg.get('MAIL_USER'), + pass: this.cfg.get('MAIL_PASSWORD'), + }, + }); + } + + private validateEmail(email: string) { + const isValid = validator.validate(email); + if (!isValid) { + throw new BadRequestException('Invalid email address'); + } + } + + private async sendMail(to: string, subject: string, templateName: string, context: any) { + this.validateEmail(to); + + const templatePath = path.join(process.cwd(), 'templates', `${templateName}.hbs`); + const templateSource = fs.readFileSync(templatePath, 'utf8'); + + const template = hbs.compile(templateSource); + const html = template(context); + + return await this.transporter.sendMail({ + from: `"${this.cfg.get('MAIL_FROM_NAME')}" <${this.cfg.get('MAIL_FROM_EMAIL')}>`, + to, + subject, + html, + }); + } + + async sendRegistrationCode(email: string, name: string, code: string) { + const codeArray = code.toString().split(''); + + return this.sendMail(email, 'Код подтверждения регистрации', 'confirmation', { + name, + codeArray, + }); + } + + async sendResetPasswordCode(email: string, code: string) { + const codeArray = code.toString().split(''); + + return this.sendMail(email, 'Восстановление пароля', 'reset-password', { + codeArray, + }); + } +} diff --git a/templates/confirmation.hbs b/templates/confirmation.hbs new file mode 100644 index 0000000..7406bdf --- /dev/null +++ b/templates/confirmation.hbs @@ -0,0 +1,57 @@ + + + + + + + +
+
+

Task Tracker

+
+
+

Проверка безопасности

+

Привет, {{name}}! Используйте этот код для подтверждения:

+ +
+ {{#each codeArray}} +
{{this}}
+ {{/each}} +
+ +

Код будет активен в течение 15 минут.

+
+ +
+ + \ No newline at end of file diff --git a/templates/reset-password.hbs b/templates/reset-password.hbs new file mode 100644 index 0000000..37fbaab --- /dev/null +++ b/templates/reset-password.hbs @@ -0,0 +1,56 @@ + + + + + + + +
+
+

Task Tracker

+
+
+

Сброс пароля

+

Здравствуйте!

+

Мы получили запрос на восстановление пароля для вашего аккаунта.
Ваш одноразовый код для сброса:

+ +
+ {{#each codeArray}} +
{{this}}
+ {{/each}} +
+ +

Никому не сообщайте этот код. Если вы не запрашивали сброс пароля, немедленно обратитесь в поддержку.

+
+ +
+ + \ No newline at end of file From 84d97647a6f7226de5bd203b4cd3ac7f06fd475a Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 02:04:03 +0300 Subject: [PATCH 34/47] feat(user): implement user controller with facade integration --- .../user/controller/user.controller.ts | 73 +++---------------- src/modules/user/controller/user.swagger.ts | 24 ++++-- src/modules/user/user.service.ts | 28 +++++-- src/shared/decorators/index.ts | 1 + src/shared/decorators/user.decorator.ts | 24 ++++++ src/shared/dtos/index.ts | 1 + src/shared/dtos/pagination.dto.ts | 9 +++ src/shared/types/fastify.d.ts | 7 ++ 8 files changed, 91 insertions(+), 76 deletions(-) create mode 100644 src/shared/decorators/user.decorator.ts create mode 100644 src/shared/dtos/index.ts create mode 100644 src/shared/dtos/pagination.dto.ts create mode 100644 src/shared/types/fastify.d.ts diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts index cfa6fb6..7ac23cf 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/modules/user/controller/user.controller.ts @@ -1,6 +1,5 @@ import { Body, Get, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { UserService } from '../user.service'; -import { createId } from '@paralleldrive/cuid2'; import { GetMeActivitySwagger, GetMeSwagger, @@ -9,8 +8,9 @@ import { PostMeAvatarSwagger, } from './user.swagger'; import { UpdateNotificationsDto, UpdateProfileDto } from '../dtos'; -import { ApiBaseController } from '../../../shared/decorators'; +import { ApiBaseController, GetUserId } from '../../../shared/decorators'; import { BearerAuthGuard } from 'src/shared/guards'; +import { PaginationDto } from '../../../shared/dtos'; @ApiBaseController('users', 'Users') @UseGuards(BearerAuthGuard) @@ -19,79 +19,26 @@ export class UserController { @Get('me') @GetMeSwagger() - async getProfile() { - return { - id: createId(), - fullName: 'Alexey Smirnov', - email: 'alexey.smirnov@example.com', - bio: 'Менеджер продукта с 5-летним опытом создания SaaS-платформ. Увлечён продуктивностью и чистым дизайном.', - avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', - timezone: 'Europe/Moscow', - language: 'ru', - security: { - is2faEnabled: true, - lastPasswordChange: new Date('2023-10-24').toISOString(), - }, - notifications: { - email: { task_assigned: true, mentions: true, daily_summary: false }, - push: { task_assigned: true, reminders: true }, - }, - }; + async getProfile(@GetUserId() id: string) { + return this.facade.getProfile(id); } @Patch('me') @PatchMeSwagger() - async updateProfile(@Body() dto: UpdateProfileDto) { - return { - success: true, - message: 'Профиль успешно обновлен.', - updatedAt: new Date().toISOString(), - data: dto, - }; + async updateProfile(@Body() dto: UpdateProfileDto, @GetUserId() id: string) { + return this.facade.updateProfile(id, dto); } @Patch('me/notifications') @PatchMeNotificationsSwagger() - async updateNotifications(@Body() settings: UpdateNotificationsDto) { - return { - success: true, - newSettings: settings, - }; + async updateNotifications(@Body() settings: UpdateNotificationsDto, @GetUserId() id: string) { + return this.facade.updateNotifications(id, settings); } @Get('me/activity') @GetMeActivitySwagger() - async getActivity(@Query('limit') limit: string) { - return [ - { - id: createId(), - eventType: 'TASK_COMPLETED', - description: 'Завершена задача "Обновить текст лендинга"', - createdAt: new Date(Date.now() - 1000 * 60 * 120).toISOString(), - metadata: { taskId: createId() }, - }, - { - id: createId(), - eventType: 'SECURITY_UPDATE', - description: 'Вы изменили настройки пароля', - createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), - metadata: null, - }, - { - id: createId(), - eventType: 'COMMENT_ADDED', - description: 'Вы прокомментировали "Проверка дизайн-системы"', - createdAt: '2023-10-24T14:30:00Z', - metadata: { taskId: createId() }, - }, - { - id: createId(), - eventType: 'AVATAR_UPLOADED', - description: 'Вы загрузили новую фотографию профиля', - createdAt: '2023-10-22T10:00:00Z', - metadata: null, - }, - ].slice(0, Number(limit) || 10); + async getActivity(@Query() query: PaginationDto, @GetUserId() id: string) { + return this.facade.getActivity(id, query.page, query.limit); } @Post('me/avatar') diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts index 8ca6e4f..6479940 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/modules/user/controller/user.swagger.ts @@ -98,15 +98,23 @@ export const GetMeActivitySwagger = () => status: 200, description: 'Список активностей успешно получен.', schema: { - example: [ - { - id: 'clj1abc230000jk78', - eventType: 'TASK_COMPLETED', - description: 'Завершена задача "Обновить текст лендинга"', - createdAt: '2026-04-10T20:00:00.000Z', - metadata: { taskId: 'clj1xyz990000abc1' }, + example: { + data: [ + { + id: 'clj1abc230000jk78', + eventType: 'TASK_COMPLETED', + description: 'Завершена задача "Обновить текст лендинга"', + createdAt: '2026-04-10T20:00:00.000Z', + metadata: { taskId: 'clj1xyz990000abc1' }, + }, + ], + meta: { + total: 45, + page: 1, + limit: 20, + totalPages: 3, }, - ], + }, }, }), ApiUnauthorized(), diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 57a283f..60bfa17 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -36,7 +36,12 @@ export class UserService { metadata: { fields: Object.keys(dto) }, }); - return updatedUser; + return { + success: true, + message: 'Профиль успешно обновлен', + updatedAt: new Date().toISOString(), + data: updatedUser, + }; }; public updateNotifications = async (id: string, dto: UpdateNotificationsDto) => { @@ -44,7 +49,7 @@ export class UserService { if (!user) this.throwUserNotFound(); - await this.userRepo.updateNotifications(id, { + const settings = await this.userRepo.updateNotifications(id, { email: dto.email, push: dto.push, }); @@ -55,17 +60,30 @@ export class UserService { eventType: 'NOTIFICATIONS_UPDATED', }); - return { success: true }; + return { + success: true, + newSettings: settings, + }; }; - public getActivity = async (id: string, page: number = 1, limit: number = 20) => { + public getActivity = async (id: string, page: number, limit: number) => { const safeLimit = Math.min(limit, 50); const offset = (page - 1) * safeLimit; - return await this.userRepo.findActivityByUser(id, { + const { items, total } = await this.userRepo.findActivityByUser(id, { limit: safeLimit, offset, }); + + return { + items, + meta: { + total, + page, + limit: safeLimit, + totalPages: Math.ceil(total / safeLimit), + }, + }; }; public uploadAvatar = async (id: string, avatarUrl: string) => { diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts index e9b9502..c2f9d19 100644 --- a/src/shared/decorators/index.ts +++ b/src/shared/decorators/index.ts @@ -1 +1,2 @@ export { ApiBaseController } from './api-controller.decorator'; +export * from './user.decorator'; diff --git a/src/shared/decorators/user.decorator.ts b/src/shared/decorators/user.decorator.ts new file mode 100644 index 0000000..7fc2467 --- /dev/null +++ b/src/shared/decorators/user.decorator.ts @@ -0,0 +1,24 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { FastifyRequest } from 'fastify'; +import { JwtPayload } from '../../modules/auth/types'; + +export const GetUser = createParamDecorator( + (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + + const user = request.user as JwtPayload; + + if (!user) return null; + + return data ? user[data] : user; + }, +); + +export const GetUserId = createParamDecorator( + (_data: unknown, ctx: ExecutionContext): string | undefined => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user as JwtPayload; + + return user?.sub; + }, +); diff --git a/src/shared/dtos/index.ts b/src/shared/dtos/index.ts new file mode 100644 index 0000000..5f10edb --- /dev/null +++ b/src/shared/dtos/index.ts @@ -0,0 +1 @@ +export * from './pagination.dto'; diff --git a/src/shared/dtos/pagination.dto.ts b/src/shared/dtos/pagination.dto.ts new file mode 100644 index 0000000..d0e8d38 --- /dev/null +++ b/src/shared/dtos/pagination.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const PaginationSchema = z.object({ + page: z.coerce.number().int().min(1).default(1), + limit: z.coerce.number().int().min(1).max(100).default(20), +}); + +export class PaginationDto extends createZodDto(PaginationSchema) {} diff --git a/src/shared/types/fastify.d.ts b/src/shared/types/fastify.d.ts new file mode 100644 index 0000000..db45904 --- /dev/null +++ b/src/shared/types/fastify.d.ts @@ -0,0 +1,7 @@ +import { JwtPayload } from './jwt-payload.type'; + +declare module 'fastify' { + interface FastifyRequest { + user?: JwtPayload; + } +} From 552b08bdf5158e5868fcbae8172746d0aded80f7 Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 02:01:56 +0300 Subject: [PATCH 35/47] feat(auth): integrate logic per reset password flow #3 / bug with otplib always code apply --- package.json | 5 + pnpm-lock.yaml | 264 +++++++++++++++++- src/modules/app/app.controller.spec.ts | 20 -- src/modules/app/app.controller.ts | 11 - src/modules/app/app.module.ts | 27 +- src/modules/auth/auth.module.ts | 11 + .../auth/controller/auth.controller.ts | 57 ++-- src/modules/auth/dtos/password.dto.ts | 30 ++ src/modules/auth/services/auth.service.ts | 160 ++++++++++- src/modules/mail/index.ts | 1 - src/modules/mail/mail.module.ts | 9 - src/modules/user/commands/index.ts | 1 + .../user/commands/update-pass.command.ts | 24 ++ src/modules/user/index.ts | 2 +- src/modules/user/user.module.ts | 4 +- .../adapters/mail/adapter.ts} | 14 +- src/shared/adapters/mail/index.ts | 2 + src/shared/adapters/mail/port.ts | 4 + src/shared/workers/enum.ts | 9 + src/shared/workers/events/index.ts | 2 + .../workers/events/register-code.event.ts | 7 + .../workers/events/reset-password.event.ts | 6 + src/shared/workers/index.ts | 3 + src/shared/workers/mail/index.ts | 1 + src/shared/workers/mail/worker.ts | 72 +++++ 25 files changed, 644 insertions(+), 102 deletions(-) delete mode 100644 src/modules/app/app.controller.spec.ts delete mode 100644 src/modules/app/app.controller.ts delete mode 100644 src/modules/mail/index.ts delete mode 100644 src/modules/mail/mail.module.ts create mode 100644 src/modules/user/commands/update-pass.command.ts rename src/{modules/mail/mail.service.ts => shared/adapters/mail/adapter.ts} (83%) create mode 100644 src/shared/adapters/mail/index.ts create mode 100644 src/shared/adapters/mail/port.ts create mode 100644 src/shared/workers/enum.ts create mode 100644 src/shared/workers/events/index.ts create mode 100644 src/shared/workers/events/register-code.event.ts create mode 100644 src/shared/workers/events/reset-password.event.ts create mode 100644 src/shared/workers/index.ts create mode 100644 src/shared/workers/mail/index.ts create mode 100644 src/shared/workers/mail/worker.ts diff --git a/package.json b/package.json index 4849ac0..f8f63d4 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,15 @@ "prepare": "husky" }, "dependencies": { + "@bull-board/api": "^6.21.0", + "@bull-board/fastify": "^6.21.0", + "@bull-board/nestjs": "^6.21.0", "@fastify/compress": "^8.3.1", "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.2.0", "@fastify/static": "^9.1.0", "@nestjs-modules/ioredis": "^2.2.1", + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^11.1.18", "@nestjs/config": "^4.0.4", "@nestjs/core": "^11.1.18", @@ -40,6 +44,7 @@ "@paralleldrive/cuid2": "^3.3.0", "@willsoto/nestjs-prometheus": "^6.1.0", "argon2": "^0.44.0", + "bullmq": "^5.73.4", "drizzle-orm": "^0.45.2", "drizzle-zod": "^0.8.3", "email-validator": "^2.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e91630..ff2f14f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@bull-board/api': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/fastify': + specifier: ^6.21.0 + version: 6.21.0 + '@bull-board/nestjs': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) '@fastify/compress': specifier: ^8.3.1 version: 8.3.1 @@ -23,6 +32,9 @@ importers: '@nestjs-modules/ioredis': specifier: ^2.2.1 version: 2.2.1(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/bullmq': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.73.4) '@nestjs/common': specifier: ^11.1.18 version: 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -40,7 +52,7 @@ importers: version: 11.0.5(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': specifier: ^11.1.18 - version: 11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 11.1.18(@fastify/static@9.1.0)(@fastify/view@11.1.1)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/swagger': specifier: ^11.2.7 version: 11.2.7(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) @@ -56,6 +68,9 @@ importers: argon2: specifier: ^0.44.0 version: 0.44.0 + bullmq: + specifier: ^5.73.4 + version: 5.73.4 drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.20.0) @@ -260,6 +275,27 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@bull-board/api@6.21.0': + resolution: {integrity: sha512-5bX3U8baU4OulDLeXwqWI6/FZolpi1APfoJVXndR4fKdmuYr9cdbH8cg7juublfzX01T+3zoiZkveX7iD5y8gA==} + peerDependencies: + '@bull-board/ui': 6.21.0 + + '@bull-board/fastify@6.21.0': + resolution: {integrity: sha512-2Og70c0Br9fKF6cX5MKLt2WTvGw3yiu+4OG2K8UAE+yFBrm+VNHxEmfvXvsyoVlnT1bzBpLzaxqC21NWCzY6SA==} + + '@bull-board/nestjs@6.21.0': + resolution: {integrity: sha512-h4UhJw9Hc4ehQcs4y+fd7CgSTyIxHN1uFttwWiFuPpMkA+t5/OcAdlB0THigjxwmL2vYgcFzuk9nKb0qHtlRkw==} + peerDependencies: + '@bull-board/api': ^6.21.0 + '@nestjs/bull-shared': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.8.1 + + '@bull-board/ui@6.21.0': + resolution: {integrity: sha512-SemKRipdrZVqboae/Xhl7CTdIwWJ+F3G/DEP7XHi1Qt1kXZUIKJkySXlFHILunygCiHRpCJ6/Ax/XNdHI/n3QA==} + '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} @@ -873,6 +909,9 @@ packages: '@fastify/static@9.1.0': resolution: {integrity: sha512-EPRNQYqEYEYTK8yyGbcM0iHpyJaupb94bey5O6iCQfLTADr02kaZU+qeHSdd9H9TiMwTBVkrMa59V8CMbn3avQ==} + '@fastify/view@11.1.1': + resolution: {integrity: sha512-GiHqT3R2eKJgWmy0s45eELTC447a4+lTM2o+8fSWeKwBe9VToeePuHJcKtOEXPrKGSddGO0RsNayULiS3aeHeQ==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1065,6 +1104,36 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + '@napi-rs/wasm-runtime@1.1.3': resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} peerDependencies: @@ -1078,6 +1147,19 @@ packages: '@nestjs/core': '>=6.7.0' ioredis: '>=5.0.0' + '@nestjs/bull-shared@11.0.4': + resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + + '@nestjs/bullmq@11.0.4': + resolution: {integrity: sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + '@nestjs/cli@11.0.19': resolution: {integrity: sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==} engines: {node: '>= 20.11'} @@ -1928,6 +2010,9 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1997,6 +2082,9 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bullmq@5.73.4: + resolution: {integrity: sha512-Q+NeFLtdKSD3GDPYSX4pH+Mc9E4OZVKimXwrnZ5WmndNy31COMy4vQV9zfhgfHGSUFrlpsBicfKYbSjx9FbO+A==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2180,6 +2268,10 @@ packages: create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-env@10.1.0: resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} engines: {node: '>=20'} @@ -2377,6 +2469,11 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + electron-to-chromium@1.5.334: resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==} @@ -2601,6 +2698,9 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -2859,6 +2959,11 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} @@ -3091,6 +3196,10 @@ packages: resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} engines: {node: 20 || >=22} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -3167,6 +3276,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -3181,6 +3294,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -3217,6 +3337,10 @@ packages: node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -3476,6 +3600,9 @@ packages: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} + redis-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + redis-parser@3.0.0: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} @@ -3915,6 +4042,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -4181,6 +4312,32 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@bull-board/api@6.21.0(@bull-board/ui@6.21.0)': + dependencies: + '@bull-board/ui': 6.21.0 + redis-info: 3.1.0 + + '@bull-board/fastify@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/ui': 6.21.0 + '@fastify/static': 9.1.0 + '@fastify/view': 11.1.1 + ejs: 3.1.10 + + '@bull-board/nestjs@6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)))(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + + '@bull-board/ui@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@colors/colors@1.5.0': optional: true @@ -4653,6 +4810,11 @@ snapshots: fastq: 1.20.1 glob: 13.0.6 + '@fastify/view@11.1.1': + dependencies: + fastify-plugin: 5.1.0 + toad-cache: 3.7.0 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4842,6 +5004,24 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -4873,6 +5053,20 @@ snapshots: - sequelize - typeorm + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))(bullmq@5.73.4)': + dependencies: + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.73.4 + tslib: 2.8.1 + '@nestjs/cli@11.0.19(@swc/core@1.15.24)(@types/node@20.19.39)(esbuild@0.27.7)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) @@ -4949,7 +5143,7 @@ snapshots: '@nestjs/common': 11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.1.18(@fastify/static@9.1.0)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/platform-fastify@11.1.18(@fastify/static@9.1.0)(@fastify/view@11.1.1)(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18(@nestjs/common@11.1.18(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: '@fastify/cors': 11.2.0 '@fastify/formbody': 8.0.2 @@ -4965,6 +5159,7 @@ snapshots: tslib: 2.8.1 optionalDependencies: '@fastify/static': 9.1.0 + '@fastify/view': 11.1.1 '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': dependencies: @@ -5679,6 +5874,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + async@3.2.6: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -5757,6 +5954,18 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bullmq@5.73.4: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.10.1 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.4 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -5912,6 +6121,10 @@ snapshots: create-require@1.1.1: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-env@10.1.0: dependencies: '@epic-web/invariant': 1.0.0 @@ -6016,6 +6229,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + ejs@3.1.10: + dependencies: + jake: 10.9.4 + electron-to-chromium@1.5.334: {} email-validator@2.0.4: {} @@ -6330,6 +6547,10 @@ snapshots: transitivePeerDependencies: - supports-color + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6604,6 +6825,12 @@ snapshots: iterare@1.2.1: {} + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + jest-worker@27.5.1: dependencies: '@types/node': 20.19.39 @@ -6811,6 +7038,8 @@ snapshots: lru-cache@11.3.3: {} + luxon@3.7.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6872,6 +7101,10 @@ snapshots: dependencies: brace-expansion: 1.1.13 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.3 + minimatch@9.0.3: dependencies: brace-expansion: 2.0.3 @@ -6882,6 +7115,22 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + mute-stream@2.0.0: {} nanoid@3.3.11: {} @@ -6907,6 +7156,11 @@ snapshots: dependencies: lodash: 4.18.1 + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + node-gyp-build@4.8.4: {} node-releases@2.0.37: {} @@ -7173,6 +7427,10 @@ snapshots: redis-errors@1.2.0: {} + redis-info@3.1.0: + dependencies: + lodash: 4.18.1 + redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 @@ -7614,6 +7872,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + v8-compile-cache-lib@3.0.1: {} vite@8.0.8(@types/node@20.19.39)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): diff --git a/src/modules/app/app.controller.spec.ts b/src/modules/app/app.controller.spec.ts deleted file mode 100644 index 169b786..0000000 --- a/src/modules/app/app.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/src/modules/app/app.controller.ts b/src/modules/app/app.controller.ts deleted file mode 100644 index eb0cb39..0000000 --- a/src/modules/app/app.controller.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller() -export class AppController { - constructor() {} - - @Get() - getHello(): string { - return 'Hello World!'; - } -} diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index a9eea71..7cf4006 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; import { ConfigModule } from '@libs/config'; import { DatabaseModule } from '@libs/database'; import { ConfigService } from '@nestjs/config'; @@ -11,7 +10,11 @@ import { HealthModule } from '@libs/health'; import { UserModule } from '../user'; import { GlobalExceptionFilter } from 'src/shared/error'; import { AuthModule } from '../auth'; -import { MailModule } from '../mail'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { FastifyAdapter } from '@bull-board/fastify'; +import { MailProcessor } from 'src/shared/workers'; +import { BullModule } from '@nestjs/bullmq'; +import { MailAdapter } from 'src/shared/adapters/mail'; @Module({ imports: [ @@ -35,13 +38,29 @@ import { MailModule } from '../mail'; }; }, }), + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: (cfg: ConfigService) => ({ + connection: { + host: cfg.getOrThrow('REDIS_HOST'), + port: cfg.getOrThrow('REDIS_PORT'), + }, + }), + }), AuthModule, UserModule, - MailModule, + BullBoardModule.forRoot({ + route: '/queues', + adapter: FastifyAdapter, + }), HealthModule.register('gateway'), ], - controllers: [AppController], providers: [ + { + provide: 'IMailPort', + useClass: MailAdapter, + }, + MailProcessor, { provide: APP_PIPE, useClass: ZodValidationPipe, diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 970961b..14fe3ed 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -7,6 +7,10 @@ import { ConfigService } from '@nestjs/config'; import { RedisModule } from '@nestjs-modules/ioredis'; import { SessionRepository } from './repository'; import { BearerStrategy, CookieStrategy } from './strategies'; +import { BullModule } from '@nestjs/bullmq'; +import { Queues } from 'src/shared/workers'; +import { BullBoardModule } from '@bull-board/nestjs'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; @Module({ imports: [ @@ -48,6 +52,13 @@ import { BearerStrategy, CookieStrategy } from './strategies'; }; }, }), + BullModule.registerQueue({ + name: Queues.MAIL, + }), + BullBoardModule.forFeature({ + name: Queues.MAIL, + adapter: BullMQAdapter, + }), forwardRef(() => UserModule), ], controllers: [AuthController], diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 1344ba7..1856b8e 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -1,19 +1,20 @@ import { ApiBaseController } from '../../../shared/decorators'; -import { Body, Delete, Get, HttpCode, Patch, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { Body, HttpCode, Post, Req, Res, UseGuards } from '@nestjs/common'; import { AuthService } from '../services/auth.service'; import { - DeleteTerminateSessionSwagger, - GetSessionsSwagger, - PostChangePasswordSwagger, - PostConfirm2faSwagger, - PostDisable2faSwagger, - PostEnable2faSwagger, PostLoginSwagger, PostLogoutSwagger, PostRefreshSwagger, PostRegisterSwagger, } from './auth.swagger'; -import { SignInDto, SignUpDto, VerifyDto } from '../dtos'; +import { + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from '../dtos'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { getDeviceMeta } from '../helpers'; import { BearerAuthGuard, CookieAuthGuard } from 'src/shared/guards'; @@ -29,7 +30,7 @@ export class AuthController { return this.facade.signUp(dto); } - @Post('verify') + @Post('sign-up/confirm') @PostRegisterSwagger() @HttpCode(201) async verify( @@ -58,7 +59,7 @@ export class AuthController { @Body() dto: SignInDto, ) { const meta = getDeviceMeta(req); - const { tokens, ...response } = await this.facade.sigIn(dto, meta); + const { tokens, ...response } = await this.facade.signIn(dto, meta); res.setCookie('refresh', tokens.refresh, { httpOnly: true, @@ -101,30 +102,18 @@ export class AuthController { return { token: tokens.access, ...response }; } - @Get('sessions') - @GetSessionsSwagger() - async getSessions() {} - - @Delete('sessions/:cuid') - @DeleteTerminateSessionSwagger() - async terminateSession() {} - - @Post('change-password') - @PostChangePasswordSwagger() - @HttpCode(200) - async changePassword() {} - - @Post('2fa/enable') - @HttpCode(200) - @PostEnable2faSwagger() - async enable2fa() {} + @Post('password/reset') + async resetPasswordRequest(@Body() dto: ResetPasswordDto) { + return this.facade.resetPass(dto); + } - @Patch('2fa/disable') - @PostDisable2faSwagger() - async disable2fa() {} + @Post('password/reset/verify') + async verifyResetCode(@Body() dto: VerifyResetCodeDto) { + return this.facade.verifyResetPassword(dto); + } - @Post('2fa/confirm') - @HttpCode(200) - @PostConfirm2faSwagger() - async confirm2fa() {} + @Post('password/reset/confirm') + async confirmPasswordReset(@Body() dto: PasswordResetConfirmDto) { + return this.facade.confirmResetPass(dto); + } } diff --git a/src/modules/auth/dtos/password.dto.ts b/src/modules/auth/dtos/password.dto.ts index d1c3643..e0a260f 100644 --- a/src/modules/auth/dtos/password.dto.ts +++ b/src/modules/auth/dtos/password.dto.ts @@ -13,3 +13,33 @@ export const ChangePasswordSchema = z .describe('Схема смены пароля'); export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} + +export const ResetPasswordSchema = z.object({ + email: z.string().email('Некорректный формат email').describe('Email для восстановления'), +}); + +export class ResetPasswordDto extends createZodDto(ResetPasswordSchema) {} + +export const VerifyResetCodeSchema = z.object({ + email: z.string().email(), + code: z.string().length(6, 'Код должен содержать 6 цифр').describe('Код из письма'), +}); + +export class VerifyResetCodeDto extends createZodDto(VerifyResetCodeSchema) {} + +export const PasswordResetConfirmSchema = z + .object({ + email: z.string().email(), + password: z + .string() + .min(8, 'Минимум 8 символов') + .max(32, 'Максимум 32 символа') + .describe('Новый пароль'), + confirmPassword: z.string().describe('Повторите новый пароль'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Пароли не совпадают', + path: ['confirmPassword'], + }); + +export class PasswordResetConfirmDto extends createZodDto(PasswordResetConfirmSchema) {} diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 4ef6471..81c4724 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,21 +1,35 @@ import { BadRequestException, ConflictException, + ForbiddenException, Inject, Injectable, + NotFoundException, UnauthorizedException, UnprocessableEntityException, } from '@nestjs/common'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; -import { SignInDto, SignUpDto, VerifyDto } from '../dtos'; +import { + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from '../dtos'; import { validate } from 'email-validator'; import { generate, generateSecret, verify as verifyOTP } from 'otplib'; import * as argon from 'argon2'; -import { CreateUserCommand, FindOneUserCommand } from '../../user'; +import { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from '../../user'; import { TokenService } from './token.service'; import { ISessionRepository } from '../repository'; import { DeviceMetadata } from '../helpers'; +import { InjectQueue } from '@nestjs/bullmq'; +import { Queues, RegisterCodeEvent } from 'src/shared/workers'; +import type { Queue } from 'bullmq'; +import { MailJobs } from 'src/shared/workers/enum'; +import { ResetPasswordEvent } from 'src/shared/workers/events'; @Injectable() export class AuthService { @@ -24,9 +38,12 @@ export class AuthService { private readonly redis: Redis, @Inject('ISessionRepository') private readonly sessionRepo: ISessionRepository, + @InjectQueue(Queues.MAIL) + private readonly mailQueue: Queue, private readonly tokenService: TokenService, private readonly findUserCommand: FindOneUserCommand, private readonly createUserCommand: CreateUserCommand, + private readonly updateUserPass: UpdatePassUserCommand, ) {} public signUp = async (dto: SignUpDto) => { @@ -67,11 +84,16 @@ export class AuthService { otp: { token, secret }, }; - console.log(data); - await this.redis.set(`reg:${dto.email}`, JSON.stringify(data), 'EX', 900); - // this.mailService.sendOtp(dto.email, otp); + const event = new RegisterCodeEvent(dto.email, dto.firstName, token); + await this.mailQueue.add(MailJobs.SEND_REGISTER_CODE, event, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000, + }, + }); return { success: true, @@ -130,7 +152,7 @@ export class AuthService { }; }; - public sigIn = async (dto: SignInDto, meta: DeviceMetadata) => { + public signIn = async (dto: SignInDto, meta: DeviceMetadata) => { const user = await this.findUserCommand.execute({ email: dto.email }); if (!user) { @@ -235,4 +257,130 @@ export class AuthService { return { success: true, message: 'Успешно вышли из системы!' }; }; + + public resetPass = async (dto: ResetPasswordDto) => { + const isValidEmail = validate(dto.email); + + if (!isValidEmail) { + throw new UnprocessableEntityException({ + code: 'INVALID_EMAIL_FORMAT', + message: 'Указанный email адрес имеет некорректный формат', + details: { email: dto.email }, + }); + } + + const user = await this.findUserCommand.execute({ email: dto.email }); + + if (!user) { + throw new NotFoundException({ + code: 'USER_NOT_FOUND', + message: 'Пользователь с таким email не найден', + details: { email: dto.email }, + }); + } + + const secret = generateSecret(); + const token = await generate({ + secret, + digits: 6, + period: 900, + strategy: 'totp', + }); + + const resetPayload = { + email: user.email, + otp: { secret, token }, + isVerified: false, + }; + + await this.redis.set(`pass:reset:${dto.email}`, JSON.stringify(resetPayload), 'EX', 900); + + const event = new ResetPasswordEvent(dto.email, token); + await this.mailQueue.add(MailJobs.SEND_RESET_PASSWORD, event, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 5000, + }, + }); + + return { + success: true, + message: 'Код для восстановления пароля отправлен на вашу почту', + }; + }; + + public verifyResetPassword = async (dto: VerifyResetCodeDto) => { + const redisKey = `pass:reset:${dto.email}`; + const cachedData = await this.redis.get(redisKey); + + if (!cachedData) { + throw new BadRequestException({ + code: 'RESET_SESSION_EXPIRED', + message: 'Время подтверждения истекло или запрос не найден. Запросите код снова.', + }); + } + + const resetSession = JSON.parse(cachedData); + + const isValid = await verifyOTP({ + token: dto.code, + secret: resetSession.otp.secret, + digits: 6, + period: 900, + strategy: 'totp', + }); + + console.log(isValid); + + if (!isValid) { + throw new BadRequestException({ + code: 'INVALID_VERIFICATION_CODE', + message: 'Неверный или истекший код подтверждения', + }); + } + + await this.redis.set( + redisKey, + JSON.stringify({ ...resetSession, isVerified: true }), + 'EX', + 600, + ); + + return { + success: true, + message: 'Код успешно подтвержден. Теперь вы можете установить новый пароль.', + }; + }; + + public confirmResetPass = async (dto: PasswordResetConfirmDto) => { + const redisKey = `pass:reset:${dto.email}`; + const cachedData = await this.redis.get(redisKey); + + if (!cachedData) { + throw new BadRequestException({ + code: 'RESET_SESSION_NOT_FOUND', + message: 'Сессия восстановления не найдена или истекла. Начните процесс заново.', + }); + } + + const resetSession = JSON.parse(cachedData); + + if (!resetSession.isVerified) { + throw new ForbiddenException({ + code: 'CODE_NOT_VERIFIED', + message: 'Код подтверждения еще не был верифицирован.', + }); + } + + const hashed = await argon.hash(dto.password); + + await this.updateUserPass.execute(dto.email, hashed); + await this.redis.del(redisKey); + + return { + success: true, + message: 'Пароль успешно изменен. Теперь вы можете войти в аккаунт.', + }; + }; } diff --git a/src/modules/mail/index.ts b/src/modules/mail/index.ts deleted file mode 100644 index 5d54413..0000000 --- a/src/modules/mail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MailModule } from './mail.module'; diff --git a/src/modules/mail/mail.module.ts b/src/modules/mail/mail.module.ts deleted file mode 100644 index 09f6249..0000000 --- a/src/modules/mail/mail.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Global, Module } from '@nestjs/common'; -import { MailService } from './mail.service'; - -@Global() -@Module({ - providers: [MailService], - exports: [MailService], -}) -export class MailModule {} diff --git a/src/modules/user/commands/index.ts b/src/modules/user/commands/index.ts index 0400d0a..7a59139 100644 --- a/src/modules/user/commands/index.ts +++ b/src/modules/user/commands/index.ts @@ -1,2 +1,3 @@ export { CreateUserCommand } from './create.command'; export { FindOneUserCommand } from './find-one.command'; +export { UpdatePassUserCommand } from './update-pass.command'; diff --git a/src/modules/user/commands/update-pass.command.ts b/src/modules/user/commands/update-pass.command.ts new file mode 100644 index 0000000..0dd04f2 --- /dev/null +++ b/src/modules/user/commands/update-pass.command.ts @@ -0,0 +1,24 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { IUserRepository } from '../repository/user.repository.interface'; + +@Injectable() +export class UpdatePassUserCommand { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + async execute(email: string, password: string) { + const user = await this.repository.findByEmail(email); + + if (!user) { + throw new NotFoundException({ + code: 'USER_NOT_FOUND', + message: 'Пользователь для обновления пароля не найден', + details: { email }, + }); + } + + await this.repository.updatePasswordHash(user.id, password); + } +} diff --git a/src/modules/user/index.ts b/src/modules/user/index.ts index 009182f..3b9d53d 100644 --- a/src/modules/user/index.ts +++ b/src/modules/user/index.ts @@ -1,3 +1,3 @@ export { UserModule } from './user.module'; export { UserRepository } from './repository/user.repository'; -export { CreateUserCommand, FindOneUserCommand } from './commands'; +export { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; diff --git a/src/modules/user/user.module.ts b/src/modules/user/user.module.ts index 459ee82..a5b7941 100644 --- a/src/modules/user/user.module.ts +++ b/src/modules/user/user.module.ts @@ -2,14 +2,14 @@ import { Module } from '@nestjs/common'; import { UserController } from './controller'; import { UserService } from './user.service'; import { UserRepository } from './repository/user.repository'; -import { CreateUserCommand, FindOneUserCommand } from './commands'; +import { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; const REPOSITORY = { provide: 'IUserRepository', useClass: UserRepository, }; -const COMMANDS = [CreateUserCommand, FindOneUserCommand]; +const COMMANDS = [CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand]; @Module({ imports: [], diff --git a/src/modules/mail/mail.service.ts b/src/shared/adapters/mail/adapter.ts similarity index 83% rename from src/modules/mail/mail.service.ts rename to src/shared/adapters/mail/adapter.ts index 5383dd8..eadbdf9 100644 --- a/src/modules/mail/mail.service.ts +++ b/src/shared/adapters/mail/adapter.ts @@ -4,11 +4,10 @@ import * as nodemailer from 'nodemailer'; import * as hbs from 'handlebars'; import * as fs from 'fs'; import * as path from 'path'; -import * as validator from 'email-validator'; -import { BadRequestException } from '@nestjs/common'; +import { IMailPort } from './port'; @Injectable() -export class MailService { +export class MailAdapter implements IMailPort { private transporter: nodemailer.Transporter; constructor(private cfg: ConfigService) { @@ -23,16 +22,7 @@ export class MailService { }); } - private validateEmail(email: string) { - const isValid = validator.validate(email); - if (!isValid) { - throw new BadRequestException('Invalid email address'); - } - } - private async sendMail(to: string, subject: string, templateName: string, context: any) { - this.validateEmail(to); - const templatePath = path.join(process.cwd(), 'templates', `${templateName}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); diff --git a/src/shared/adapters/mail/index.ts b/src/shared/adapters/mail/index.ts new file mode 100644 index 0000000..f798bbb --- /dev/null +++ b/src/shared/adapters/mail/index.ts @@ -0,0 +1,2 @@ +export { MailAdapter } from './adapter'; +export { IMailPort } from './port'; diff --git a/src/shared/adapters/mail/port.ts b/src/shared/adapters/mail/port.ts new file mode 100644 index 0000000..8a0de98 --- /dev/null +++ b/src/shared/adapters/mail/port.ts @@ -0,0 +1,4 @@ +export interface IMailPort { + sendRegistrationCode(email: string, name: string, code: string): Promise; + sendResetPasswordCode(email: string, code: string): Promise; +} diff --git a/src/shared/workers/enum.ts b/src/shared/workers/enum.ts new file mode 100644 index 0000000..dffe92b --- /dev/null +++ b/src/shared/workers/enum.ts @@ -0,0 +1,9 @@ +export enum Queues { + MAIL = 'MAIL_QUEUE', +} + +export enum MailJobs { + SEND_REGISTER_CODE = 'SEND_REGISTER_CODE', + SEND_RESET_PASSWORD = 'SEND_RESET_PASSWORD', + SEND_CHANGE_EMAIL = 'SEND_CHANGE_EMAIL', +} diff --git a/src/shared/workers/events/index.ts b/src/shared/workers/events/index.ts new file mode 100644 index 0000000..61a6360 --- /dev/null +++ b/src/shared/workers/events/index.ts @@ -0,0 +1,2 @@ +export { RegisterCodeEvent } from './register-code.event'; +export { ResetPasswordEvent } from './reset-password.event'; diff --git a/src/shared/workers/events/register-code.event.ts b/src/shared/workers/events/register-code.event.ts new file mode 100644 index 0000000..df87ca8 --- /dev/null +++ b/src/shared/workers/events/register-code.event.ts @@ -0,0 +1,7 @@ +export class RegisterCodeEvent { + constructor( + public email: string, + public name: string, + public otp: string, + ) {} +} diff --git a/src/shared/workers/events/reset-password.event.ts b/src/shared/workers/events/reset-password.event.ts new file mode 100644 index 0000000..1f50e09 --- /dev/null +++ b/src/shared/workers/events/reset-password.event.ts @@ -0,0 +1,6 @@ +export class ResetPasswordEvent { + constructor( + public email: string, + public otp: string, + ) {} +} diff --git a/src/shared/workers/index.ts b/src/shared/workers/index.ts new file mode 100644 index 0000000..2111275 --- /dev/null +++ b/src/shared/workers/index.ts @@ -0,0 +1,3 @@ +export { MailJobs, Queues } from './enum'; +export { RegisterCodeEvent } from './events'; +export { MailProcessor } from './mail'; diff --git a/src/shared/workers/mail/index.ts b/src/shared/workers/mail/index.ts new file mode 100644 index 0000000..a059e2b --- /dev/null +++ b/src/shared/workers/mail/index.ts @@ -0,0 +1 @@ +export { MailProcessor } from './worker'; diff --git a/src/shared/workers/mail/worker.ts b/src/shared/workers/mail/worker.ts new file mode 100644 index 0000000..06ce4b1 --- /dev/null +++ b/src/shared/workers/mail/worker.ts @@ -0,0 +1,72 @@ +import { Processor, WorkerHost } from '@nestjs/bullmq'; +import { MailJobs, Queues } from '../enum'; +import type { Job } from 'bullmq'; +import { IMailPort } from 'src/shared/adapters/mail'; +import { Inject } from '@nestjs/common'; +import type { RegisterCodeEvent, ResetPasswordEvent } from '../events'; + +@Processor(Queues.MAIL) +export class MailProcessor extends WorkerHost { + constructor( + @Inject('IMailPort') + private readonly mailAdapter: IMailPort, + ) { + super(); + } + + async process(job: Job): Promise; + async process(job: Job): Promise; + async process(job: Job): Promise { + await job.log(`[START] Job ID: ${job.id} | Type: ${job.name}`); + + try { + switch (job.name) { + case MailJobs.SEND_REGISTER_CODE: + await this.sendRegisterCode(job); + break; + case MailJobs.SEND_RESET_PASSWORD: + await this.sendResetPassCode(job); + break; + default: + await job.log(`[WRN] No handler for job: ${job.name}`); + await job.updateProgress(100); + } + + await job.log(`[DONE] Job ${job.id} processed`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : ''; + + await job.log(`[FAIL] ${errorMessage}`); + if (errorStack) { + await job.log(errorStack); + } + + throw error; + } + } + + private sendRegisterCode = async (job: Job) => { + const { email, name, otp } = job.data; + + await job.log(`Sending registration code to: ${email}`); + await job.updateProgress(20); + + await this.mailAdapter.sendRegistrationCode(email, name, otp); + + await job.log(`Successfully sent to ${email}`); + await job.updateProgress(100); + }; + + private sendResetPassCode = async (job: Job) => { + const { email, otp } = job.data; + + await job.log(`Sending password reset to: ${email}`); + await job.updateProgress(30); + + await this.mailAdapter.sendResetPasswordCode(email, otp); + + await job.log(`Reset link delivered to ${email}`); + await job.updateProgress(100); + }; +} From 6d4305919d4c36999ec5c89747c07e259717052f Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 04:20:02 +0300 Subject: [PATCH 36/47] fix(auth): fix OTP verification bypass in AuthService --- src/modules/auth/services/auth.service.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 81c4724..29562b1 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -115,7 +115,7 @@ export class AuthService { const userData = JSON.parse(cachedData); - const isValid = await verifyOTP({ + const verifyResult = await verifyOTP({ token: dto.code, secret: userData.otp.secret, algorithm: 'sha256', @@ -124,7 +124,7 @@ export class AuthService { strategy: 'totp', }); - if (!isValid) { + if (!verifyResult.valid) { throw new BadRequestException({ code: 'INVALID_OTP', message: 'Неверный или истекший код подтверждения', @@ -323,7 +323,7 @@ export class AuthService { const resetSession = JSON.parse(cachedData); - const isValid = await verifyOTP({ + const verifyResult = await verifyOTP({ token: dto.code, secret: resetSession.otp.secret, digits: 6, @@ -331,9 +331,7 @@ export class AuthService { strategy: 'totp', }); - console.log(isValid); - - if (!isValid) { + if (!verifyResult.valid) { throw new BadRequestException({ code: 'INVALID_VERIFICATION_CODE', message: 'Неверный или истекший код подтверждения', From 62719c4c6c1f6baf43cf30d38db381deb6866e70 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 04:37:27 +0300 Subject: [PATCH 37/47] fix(auth): correct session expiration time in signIn and verify --- src/modules/auth/services/auth.service.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 29562b1..d869b4b 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -138,7 +138,7 @@ export class AuthService { const session = await this.sessionRepo.create({ userId: user.id, - expiresAt: new Date(), + expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), ...meta, }); const { access, refresh } = await this.tokenService.generateTokens(user, session.id); @@ -173,7 +173,7 @@ export class AuthService { const { id } = await this.sessionRepo.create({ userId: user.id, - expiresAt: new Date(), + expiresAt: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), ...meta, }); @@ -208,8 +208,6 @@ export class AuthService { }); } - console.log(session); - const user = await this.findUserCommand.execute({ id: session.userId }); if (!user) { From e33fde77c9c1fa5f74d76966c03aa40f894d883f Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 04:45:53 +0300 Subject: [PATCH 38/47] fix(email): fix verification code copy formatting --- templates/confirmation.hbs | 6 +----- templates/reset-password.hbs | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/templates/confirmation.hbs b/templates/confirmation.hbs index 7406bdf..c30923b 100644 --- a/templates/confirmation.hbs +++ b/templates/confirmation.hbs @@ -40,11 +40,7 @@

Проверка безопасности

Привет, {{name}}! Используйте этот код для подтверждения:

-
- {{#each codeArray}} -
{{this}}
- {{/each}} -
+
{{#each codeArray}}{{this}}{{/each}}

Код будет активен в течение 15 минут.

diff --git a/templates/reset-password.hbs b/templates/reset-password.hbs index 37fbaab..1fa520e 100644 --- a/templates/reset-password.hbs +++ b/templates/reset-password.hbs @@ -40,11 +40,7 @@

Здравствуйте!

Мы получили запрос на восстановление пароля для вашего аккаунта.
Ваш одноразовый код для сброса:

-
- {{#each codeArray}} -
{{this}}
- {{/each}} -
+
{{#each codeArray}}
{{this}}
{{/each}}

Никому не сообщайте этот код. Если вы не запрашивали сброс пароля, немедленно обратитесь в поддержку.

From 64de91e37cf0c1054880f3f7b7670557dd064a26 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 18:31:09 +0300 Subject: [PATCH 39/47] fix(infra): specify postgres user and db for healthcheck --- infra/compose.dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml index a3a6d64..045bce7 100644 --- a/infra/compose.dev.yaml +++ b/infra/compose.dev.yaml @@ -38,7 +38,7 @@ services: networks: - backend healthcheck: - test: ["CMD-SHELL", "pg_isready -q"] + test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\" -q || exit 1"] interval: 5s timeout: 5s retries: 5 From c358765ec425b66c7d9c7d573a2374f833979a38 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 12 Apr 2026 20:02:53 +0300 Subject: [PATCH 40/47] fix(cors): normalize origins to hostname to resolve local blocks --- .env.example | 2 +- libs/bootstrap/src/setups/cors.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index a59f112..fef55af 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # --- APP --- PORT=3000 NODE_ENV=development -CORS_ALLOWED_ORIGINS=http://localhost:3000 +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 # --- POSTGRES --- DB_USERNAME=admin diff --git a/libs/bootstrap/src/setups/cors.ts b/libs/bootstrap/src/setups/cors.ts index 73d2847..59a7959 100644 --- a/libs/bootstrap/src/setups/cors.ts +++ b/libs/bootstrap/src/setups/cors.ts @@ -11,13 +11,22 @@ export function setupCors(app: NestFastifyApplication, origins: string[]) { return callback(null, true); } - const { hostname } = new URL(origin); + try { + const { hostname } = new URL(origin); + const allowedHostnames = origins.map((o) => new URL(o).hostname); - if (origins.some((o) => hostname === o || hostname.endsWith(`.${o}`))) { - callback(null, origin); - } + if ( + allowedHostnames.some( + (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`), + ) + ) { + return callback(null, origin); + } - callback(new Error('Not allowed by CORS'), false); + callback(new Error('Not allowed by CORS'), false); + } catch (e) { + callback(new Error('Invalid origin format'), false); + } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'], From c877b81a02cb7b1871acc832acf00e247bdf46ee Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 20:08:02 +0300 Subject: [PATCH 41/47] fix(compose): resolve error with depends and correct output per users --- infra/compose.dev.yaml | 7 ++++--- src/modules/user/controller/user.swagger.ts | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml index 045bce7..88bbec3 100644 --- a/infra/compose.dev.yaml +++ b/infra/compose.dev.yaml @@ -14,9 +14,9 @@ services: - ../.env ports: - "3000:3000" - # depends_on: - # database: - # condition: service_healthy + depends_on: + database: + condition: service_healthy networks: - backend @@ -64,3 +64,4 @@ volumes: networks: backend: + name: task-tracker-gateway diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts index 6479940..d89723b 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/modules/user/controller/user.swagger.ts @@ -32,7 +32,7 @@ export const PatchMeSwagger = () => summary: 'Обновить данные профиля', description: 'Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса.', }), - ApiBody({ type: UpdateProfileDto }), + ApiBody({ type: UpdateProfileDto.Output }), ApiResponse({ status: 200, description: 'Профиль успешно обновлен.', @@ -64,7 +64,9 @@ export const PatchMeNotificationsSwagger = () => summary: 'Обновить настройки уведомлений', description: 'Частичное обновление настроек email и push уведомлений.', }), - ApiBody({ type: UpdateNotificationsDto }), + ApiBody({ + type: UpdateNotificationsDto.Output, + }), ApiResponse({ status: 200, description: 'Настройки успешно сохранены.', From 187b12168925be02cbdf7f99392509e67b0e16f3 Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 20:53:32 +0300 Subject: [PATCH 42/47] feat(s3):chore(aws): add s3 module at libs and env file --- .env.example | 8 +- libs/config/src/config.schema.ts | 47 +- libs/s3/src/index.ts | 2 + libs/s3/src/interfaces/index.ts | 5 + libs/s3/src/interfaces/module.interface.ts | 38 + libs/s3/src/s3.constants.ts | 1 + libs/s3/src/s3.module.ts | 41 + libs/s3/src/s3.service.ts | 25 + libs/s3/tsconfig.lib.json | 9 + nest-cli.json | 97 +- package.json | 2 + pnpm-lock.yaml | 1213 ++++++++++++++++++++ src/modules/app/app.module.ts | 17 + src/modules/user/user.service.ts | 2 + tsconfig.json | 102 +- 15 files changed, 1497 insertions(+), 112 deletions(-) create mode 100644 libs/s3/src/index.ts create mode 100644 libs/s3/src/interfaces/index.ts create mode 100644 libs/s3/src/interfaces/module.interface.ts create mode 100644 libs/s3/src/s3.constants.ts create mode 100644 libs/s3/src/s3.module.ts create mode 100644 libs/s3/src/s3.service.ts create mode 100644 libs/s3/tsconfig.lib.json diff --git a/.env.example b/.env.example index fef55af..1014c59 100644 --- a/.env.example +++ b/.env.example @@ -38,4 +38,10 @@ MAIL_USER=example@gmail.com # 16x password MAIL_PASSWORD=xxxxxxxxyyyyyyyy MAIL_FROM_NAME="Task Tracker" -MAIL_FROM_EMAIL=example@gmail.com \ No newline at end of file +MAIL_FROM_EMAIL=example@gmail.com + +S3_BUCKET_NAME='' +S3_ENDPOINT='' +S3_REGION='' +S3_ACCESS_KEY='' +S3_SECRET_KEY='' \ No newline at end of file diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 43ac0c3..1f72630 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -44,12 +44,47 @@ export const ConfigSchema = z.object({ }), JWT_ACCESS_EXPIRES_IN: timeStringSchema.default('15m'), JWT_REFRESH_EXPIRES_IN: timeStringSchema.default('30d'), - MAIL_HOST: z.string().default('smtp.gmail.com'), - MAIL_PORT: z.coerce.number().default(465), - MAIL_USER: z.email('MAIL_USER must be a valid email'), - MAIL_PASSWORD: z.string().min(1, 'MAIL_PASSWORD is missing'), - MAIL_FROM_NAME: z.string().default('Foodies App'), - MAIL_FROM_EMAIL: z.email().optional(), + MAIL_HOST: z + .string({ + error: 'Mail server host (MAIL_HOST) is not specified', + }) + .min(1, 'MAIL_HOST cannot be empty'), + MAIL_PORT: z.coerce.number({ + error: 'Mail port (MAIL_PORT) is not specified', + }), + MAIL_USER: z + .string({ + error: 'Sender email (MAIL_USER) is not specified', + }) + .email('MAIL_USER must be a valid email address'), + MAIL_PASSWORD: z + .string({ + error: 'Mail password (MAIL_PASSWORD) is required', + }) + .min(1, 'Mail password cannot be empty'), + MAIL_FROM_NAME: z + .string({ + error: 'Sender name (MAIL_FROM_NAME) is not specified', + }) + .min(1, 'Sender name cannot be empty'), + MAIL_FROM_EMAIL: z.string().email('Invalid MAIL_FROM_EMAIL format').optional(), + S3_BUCKET_NAME: z + .string({ + error: "S3_BUCKET_NAME is required. Example: 'avatars'", + }) + .min(1), + S3_ENDPOINT: z + .string({ + error: "S3_ENDPOINT is required. Example: 'http://localhost:9000'", + }) + .url('S3_ENDPOINT must be a valid URL'), + S3_REGION: z.string().default('us-east-1'), + S3_ACCESS_KEY: z.string({ + error: 'S3_ACCESS_KEY is missing (MinIO root user or IAM user)', + }), + S3_SECRET_KEY: z.string({ + error: 'S3_SECRET_KEY is missing (MinIO root password or IAM secret)', + }), }); export type Config = z.infer; diff --git a/libs/s3/src/index.ts b/libs/s3/src/index.ts new file mode 100644 index 0000000..d819c35 --- /dev/null +++ b/libs/s3/src/index.ts @@ -0,0 +1,2 @@ +export * from './s3.module'; +export * from './s3.service'; diff --git a/libs/s3/src/interfaces/index.ts b/libs/s3/src/interfaces/index.ts new file mode 100644 index 0000000..073bc43 --- /dev/null +++ b/libs/s3/src/interfaces/index.ts @@ -0,0 +1,5 @@ +export type { + S3ModuleOptions, + S3ModuleAsyncOptions, + S3ModuleOptionsFactory, +} from './module.interface'; diff --git a/libs/s3/src/interfaces/module.interface.ts b/libs/s3/src/interfaces/module.interface.ts new file mode 100644 index 0000000..1edd054 --- /dev/null +++ b/libs/s3/src/interfaces/module.interface.ts @@ -0,0 +1,38 @@ +import type { S3ClientConfig } from '@aws-sdk/client-s3'; +import type { FactoryProvider, ModuleMetadata, Provider, Type } from '@nestjs/common'; + +export interface S3ConnectionOptions extends Pick< + S3ClientConfig, + 'credentials' | 'endpoint' | 'region' +> { + bucket: string; +} + +export interface S3OtherOptions extends Omit< + S3ClientConfig, + 'credentials' | 'endpoint' | 'region' +> {} + +export interface S3ModuleOptions { + connection: S3ConnectionOptions; + config?: S3OtherOptions; + global?: boolean; +} + +export interface S3ModuleOptionsFactory { + createS3Options(): Promise | S3ModuleOptions; +} + +export interface S3ModuleAsyncOptions extends Pick< + ModuleMetadata, + 'imports' +> { + useExisting?: Type; + useClass?: Type; + useFactory?: ( + ...args: T + ) => Promise> | Omit; + inject?: FactoryProvider['inject']; + global?: boolean; + extraProviders?: Provider[]; +} diff --git a/libs/s3/src/s3.constants.ts b/libs/s3/src/s3.constants.ts new file mode 100644 index 0000000..c55a9ab --- /dev/null +++ b/libs/s3/src/s3.constants.ts @@ -0,0 +1 @@ +export const S3_OPTIONS = 'S3_OPTIONS'; diff --git a/libs/s3/src/s3.module.ts b/libs/s3/src/s3.module.ts new file mode 100644 index 0000000..ee7d610 --- /dev/null +++ b/libs/s3/src/s3.module.ts @@ -0,0 +1,41 @@ +import { type DynamicModule, Module, type Provider } from '@nestjs/common'; +import type { S3ModuleOptions, S3ModuleAsyncOptions } from './interfaces'; +import { S3Service } from './s3.service'; +import { S3_OPTIONS } from './s3.constants'; + +@Module({ + providers: [S3Service], + exports: [S3Service], +}) +export class S3Module { + static register(options: S3ModuleOptions): DynamicModule { + const { global, ...config } = options; + + return { + global, + module: S3Module, + providers: [{ provide: S3_OPTIONS, useValue: config }, S3Service], + exports: [S3Service], + }; + } + + static registerAsync(options: S3ModuleAsyncOptions): DynamicModule { + const { global, imports } = options; + + return { + global, + module: S3Module, + imports: imports || [], + providers: [this.createAsyncOptionsProvider(options), S3Service], + exports: [S3Service], + }; + } + + private static createAsyncOptionsProvider(options: S3ModuleAsyncOptions): Provider { + return { + provide: S3_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } +} diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts new file mode 100644 index 0000000..3ad5182 --- /dev/null +++ b/libs/s3/src/s3.service.ts @@ -0,0 +1,25 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { S3Client } from '@aws-sdk/client-s3'; +import { S3_OPTIONS } from './s3.constants'; +import { S3ModuleOptions } from './interfaces'; + +@Injectable() +export class S3Service { + private readonly s3Client: S3Client; + public readonly bucket: string; + + constructor( + @Inject(S3_OPTIONS) + private readonly options: S3ModuleOptions, + ) { + const { bucket, credentials, endpoint, region } = options.connection; + this.bucket = bucket; + + this.s3Client = new S3Client({ + region, + endpoint, + credentials, + ...options.config, + }); + } +} diff --git a/libs/s3/tsconfig.lib.json b/libs/s3/tsconfig.lib.json new file mode 100644 index 0000000..0cd20fa --- /dev/null +++ b/libs/s3/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/s3" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/nest-cli.json b/nest-cli.json index 5881fec..572e181 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,47 +1,56 @@ { - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true, - "webpack": true - }, - "projects": { - "bootstrap": { - "type": "library", - "root": "libs/bootstrap", - "entryFile": "index", - "sourceRoot": "libs/bootstrap/src", - "compilerOptions": { - "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" - } + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "webpack": true }, - "config": { - "type": "library", - "root": "libs/config", - "entryFile": "index", - "sourceRoot": "libs/config/src", - "compilerOptions": { - "tsConfigPath": "libs/config/tsconfig.lib.json" - } - }, - "database": { - "type": "library", - "root": "libs/database", - "entryFile": "index", - "sourceRoot": "libs/database/src", - "compilerOptions": { - "tsConfigPath": "libs/database/tsconfig.lib.json" - } - }, - "health": { - "type": "library", - "root": "libs/health", - "entryFile": "index", - "sourceRoot": "libs/health/src", - "compilerOptions": { - "tsConfigPath": "libs/health/tsconfig.lib.json" - } + "projects": { + "bootstrap": { + "type": "library", + "root": "libs/bootstrap", + "entryFile": "index", + "sourceRoot": "libs/bootstrap/src", + "compilerOptions": { + "tsConfigPath": "libs/bootstrap/tsconfig.lib.json" + } + }, + "config": { + "type": "library", + "root": "libs/config", + "entryFile": "index", + "sourceRoot": "libs/config/src", + "compilerOptions": { + "tsConfigPath": "libs/config/tsconfig.lib.json" + } + }, + "database": { + "type": "library", + "root": "libs/database", + "entryFile": "index", + "sourceRoot": "libs/database/src", + "compilerOptions": { + "tsConfigPath": "libs/database/tsconfig.lib.json" + } + }, + "health": { + "type": "library", + "root": "libs/health", + "entryFile": "index", + "sourceRoot": "libs/health/src", + "compilerOptions": { + "tsConfigPath": "libs/health/tsconfig.lib.json" + } + }, + "s3": { + "type": "library", + "root": "libs/s3", + "entryFile": "index", + "sourceRoot": "libs/s3/src", + "compilerOptions": { + "tsConfigPath": "libs/s3/tsconfig.lib.json" + } + } } - } -} \ No newline at end of file +} diff --git a/package.json b/package.json index f8f63d4..829c9fd 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "prepare": "husky" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1029.0", + "@aws-sdk/s3-request-presigner": "^3.1029.0", "@bull-board/api": "^6.21.0", "@bull-board/fastify": "^6.21.0", "@bull-board/nestjs": "^6.21.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff2f14f..ec34f87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1029.0 + version: 3.1029.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1029.0 + version: 3.1029.0 '@bull-board/api': specifier: ^6.21.0 version: 6.21.0(@bull-board/ui@6.21.0) @@ -247,6 +253,173 @@ packages: resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.1029.0': + resolution: {integrity: sha512-OuA8RZTxsAaHDcI25j2NGLMaYFI2WpJdDzK3uLmVBmaHwjQKQZOUDVVBcln8pNo3IgkY+HRSJhRR4/xlM//UyQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.973.27': + resolution: {integrity: sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/crc64-nvme@3.972.6': + resolution: {integrity: sha512-NMbiqKdruhwwgI6nzBVe2jWMkXjaoQz2YOs3rFX+2F3gGyrJDkDPwMpV/RsTFeq2vAQ055wZNtOXFK4NYSkM8g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.25': + resolution: {integrity: sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.27': + resolution: {integrity: sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.29': + resolution: {integrity: sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.29': + resolution: {integrity: sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.30': + resolution: {integrity: sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.25': + resolution: {integrity: sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.29': + resolution: {integrity: sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.29': + resolution: {integrity: sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + resolution: {integrity: sha512-COToYKgquDyligbcAep7ygs48RK+mwe/IYprq4+TSrVFzNOYmzWvHf6werpnKV5VYpRiwdn+Wa5ZXkPqLVwcTg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-expect-continue@3.972.9': + resolution: {integrity: sha512-V/FNCjFxnh4VGu+HdSiW4Yg5GELihA1MIDSAdsEPvuayXBVmr0Jaa6jdLAZLH38KYXl/vVjri9DQJWnTAujHEA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.7': + resolution: {integrity: sha512-uU4/ch2CLHB8Phu1oTKnnQ4e8Ujqi49zEnQYBhWYT53zfFvtJCdGsaOoypBr8Fm/pmCBssRmGoIQ4sixgdLP9w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.9': + resolution: {integrity: sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-location-constraint@3.972.9': + resolution: {integrity: sha512-TyfOi2XNdOZpNKeTJwRUsVAGa+14nkyMb2VVGG+eDgcWG/ed6+NUo72N3hT6QJioxym80NSinErD+LBRF0Ir1w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.9': + resolution: {integrity: sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.10': + resolution: {integrity: sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.28': + resolution: {integrity: sha512-qJHcJQH9UNPUrnPlRtCozKjtqAaypQ5IgQxTNoPsVYIQeuwNIA8Rwt3NvGij1vCDYDfCmZaPLpnJEHlZXeFqmg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-ssec@3.972.9': + resolution: {integrity: sha512-wSA2BR7L0CyBNDJeSrleIIzC+DzL93YNTdfU0KPGLiocK6YsRv1nPAzPF+BFSdcs0Qa5ku5Kcf4KvQcWwKGenQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.29': + resolution: {integrity: sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.996.19': + resolution: {integrity: sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.11': + resolution: {integrity: sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1029.0': + resolution: {integrity: sha512-YbHPaha4DYgJWdPorGV5ZSCCqHafGj4GiyqXmXFlCJSsqlOd3xEcemhOZGjrB9epdiVEUtB3DDJXGYYj55ITdQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.16': + resolution: {integrity: sha512-EMdXYB4r/k5RWq86fugjRhid5JA+Z6MpS7n4sij4u5/C+STrkvuf9aFu41rJA9MjUzxCLzv8U2XL8cH2GSRYpQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1026.0': + resolution: {integrity: sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.7': + resolution: {integrity: sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.6': + resolution: {integrity: sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.9': + resolution: {integrity: sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.9': + resolution: {integrity: sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==} + + '@aws-sdk/util-user-agent-node@3.973.15': + resolution: {integrity: sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.17': + resolution: {integrity: sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1526,6 +1699,218 @@ packages: resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.4.14': + resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.14': + resolution: {integrity: sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.13': + resolution: {integrity: sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.2.13': + resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.13': + resolution: {integrity: sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.3.13': + resolution: {integrity: sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.2.13': + resolution: {integrity: sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.2.13': + resolution: {integrity: sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.16': + resolution: {integrity: sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.2.14': + resolution: {integrity: sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.13': + resolution: {integrity: sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.2.13': + resolution: {integrity: sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.13': + resolution: {integrity: sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.13': + resolution: {integrity: sha512-cNm7I9NXolFxtS20ojROddOEpSAeI1Obq6pd1Kj5HtHws3s9Fkk8DdHDfQSs5KuxCewZuVK6UqrJnfJmiMzDuQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.13': + resolution: {integrity: sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.29': + resolution: {integrity: sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.5.1': + resolution: {integrity: sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.17': + resolution: {integrity: sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.13': + resolution: {integrity: sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.13': + resolution: {integrity: sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.5.2': + resolution: {integrity: sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.13': + resolution: {integrity: sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.13': + resolution: {integrity: sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.13': + resolution: {integrity: sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.13': + resolution: {integrity: sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.2.13': + resolution: {integrity: sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.8': + resolution: {integrity: sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.13': + resolution: {integrity: sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.9': + resolution: {integrity: sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.0': + resolution: {integrity: sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.13': + resolution: {integrity: sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.45': + resolution: {integrity: sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.49': + resolution: {integrity: sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.3.4': + resolution: {integrity: sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.13': + resolution: {integrity: sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.3.1': + resolution: {integrity: sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.22': + resolution: {integrity: sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.2.15': + resolution: {integrity: sha512-oUt9o7n8hBv3BL56sLSneL0XeigZSuem0Hr78JaoK33D9oKieyCvVP8eTSe3j7g2mm/S1DvzxKieG7JEWNJUNg==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2047,6 +2432,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + boxen@5.1.2: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} @@ -2672,6 +3060,13 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + + fast-xml-parser@5.5.8: + resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + hasBin: true + fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} @@ -3420,6 +3815,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3832,6 +4231,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -4289,6 +4691,468 @@ snapshots: transitivePeerDependencies: - chokidar + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.7 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1029.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-node': 3.972.30 + '@aws-sdk/middleware-bucket-endpoint': 3.972.9 + '@aws-sdk/middleware-expect-continue': 3.972.9 + '@aws-sdk/middleware-flexible-checksums': 3.974.7 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-location-constraint': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/middleware-ssec': 3.972.9 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/signature-v4-multi-region': 3.996.16 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/eventstream-serde-browser': 4.2.13 + '@smithy/eventstream-serde-config-resolver': 4.3.13 + '@smithy/eventstream-serde-node': 4.2.13 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-blob-browser': 4.2.14 + '@smithy/hash-node': 4.2.13 + '@smithy/hash-stream-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/md5-js': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/util-waiter': 4.2.15 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.973.27': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/xml-builder': 3.972.17 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/crc64-nvme@3.972.6': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.27': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-login': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.30': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.25 + '@aws-sdk/credential-provider-http': 3.972.27 + '@aws-sdk/credential-provider-ini': 3.972.29 + '@aws-sdk/credential-provider-process': 3.972.25 + '@aws-sdk/credential-provider-sso': 3.972.29 + '@aws-sdk/credential-provider-web-identity': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.25': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/token-providers': 3.1026.0 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/crc64-nvme': 3.972.6 + '@aws-sdk/types': 3.973.7 + '@smithy/is-array-buffer': 4.2.2 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.7 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.28': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.29': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-retry': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.996.19': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.973.27 + '@aws-sdk/middleware-host-header': 3.972.9 + '@aws-sdk/middleware-logger': 3.972.9 + '@aws-sdk/middleware-recursion-detection': 3.972.10 + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/region-config-resolver': 3.972.11 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-endpoints': 3.996.6 + '@aws-sdk/util-user-agent-browser': 3.972.9 + '@aws-sdk/util-user-agent-node': 3.973.15 + '@smithy/config-resolver': 4.4.14 + '@smithy/core': 3.23.14 + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/hash-node': 4.2.13 + '@smithy/invalid-dependency': 4.2.13 + '@smithy/middleware-content-length': 4.2.13 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-retry': 4.5.1 + '@smithy/middleware-serde': 4.2.17 + '@smithy/middleware-stack': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/node-http-handler': 4.5.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.45 + '@smithy/util-defaults-mode-node': 4.2.49 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/config-resolver': 4.4.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1029.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.996.16 + '@aws-sdk/types': 3.973.7 + '@aws-sdk/util-format-url': 3.972.9 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/protocol-http': 5.3.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.16': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.28 + '@aws-sdk/types': 3.973.7 + '@smithy/protocol-http': 5.3.13 + '@smithy/signature-v4': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1026.0': + dependencies: + '@aws-sdk/core': 3.973.27 + '@aws-sdk/nested-clients': 3.996.19 + '@aws-sdk/types': 3.973.7 + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.7': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.6': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-endpoints': 3.3.4 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.9': + dependencies: + '@aws-sdk/types': 3.973.7 + '@smithy/types': 4.14.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.15': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.29 + '@aws-sdk/types': 3.973.7 + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.17': + dependencies: + '@smithy/types': 4.14.0 + fast-xml-parser: 5.5.8 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5342,6 +6206,339 @@ snapshots: '@simple-libs/stream-utils@1.2.0': {} + '@smithy/chunked-blob-reader-native@4.2.3': + dependencies: + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.4.14': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.3.4 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 + + '@smithy/core@3.23.14': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-stream': 4.5.22 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.13': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.2.13': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.3.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.2.13': + dependencies: + '@smithy/eventstream-serde-universal': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.2.13': + dependencies: + '@smithy/eventstream-codec': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.16': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.2.14': + dependencies: + '@smithy/chunked-blob-reader': 5.2.2 + '@smithy/chunked-blob-reader-native': 4.2.3 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.13': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.29': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/middleware-serde': 4.2.17 + '@smithy/node-config-provider': 4.3.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + '@smithy/url-parser': 4.2.13 + '@smithy/util-middleware': 4.2.13 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.5.1': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/node-config-provider': 4.3.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/service-error-classification': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-retry': 4.3.1 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.17': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.13': + dependencies: + '@smithy/property-provider': 4.2.13 + '@smithy/shared-ini-file-loader': 4.4.8 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.5.2': + dependencies: + '@smithy/protocol-http': 5.3.13 + '@smithy/querystring-builder': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + + '@smithy/shared-ini-file-loader@4.4.8': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.13': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.13 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.9': + dependencies: + '@smithy/core': 3.23.14 + '@smithy/middleware-endpoint': 4.4.29 + '@smithy/middleware-stack': 4.2.13 + '@smithy/protocol-http': 5.3.13 + '@smithy/types': 4.14.0 + '@smithy/util-stream': 4.5.22 + tslib: 2.8.1 + + '@smithy/types@4.14.0': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.13': + dependencies: + '@smithy/querystring-parser': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.45': + dependencies: + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.49': + dependencies: + '@smithy/config-resolver': 4.4.14 + '@smithy/credential-provider-imds': 4.2.13 + '@smithy/node-config-provider': 4.3.13 + '@smithy/property-provider': 4.2.13 + '@smithy/smithy-client': 4.12.9 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.3.4': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.13': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-retry@4.3.1': + dependencies: + '@smithy/service-error-classification': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.22': + dependencies: + '@smithy/fetch-http-handler': 5.3.16 + '@smithy/node-http-handler': 4.5.2 + '@smithy/types': 4.14.0 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-waiter@4.2.15': + dependencies: + '@smithy/types': 4.14.0 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@swc/core-darwin-arm64@1.15.24': @@ -5903,6 +7100,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bowser@2.14.1: {} + boxen@5.1.2: dependencies: ansi-align: 3.0.1 @@ -6506,6 +7705,16 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.5.8: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + fastify-plugin@5.1.0: {} fastify@5.8.4: @@ -7253,6 +8462,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -7653,6 +8864,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.2.3: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index 7cf4006..ed9676d 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -15,6 +15,7 @@ import { FastifyAdapter } from '@bull-board/fastify'; import { MailProcessor } from 'src/shared/workers'; import { BullModule } from '@nestjs/bullmq'; import { MailAdapter } from 'src/shared/adapters/mail'; +import { S3Module } from '@libs/s3'; @Module({ imports: [ @@ -38,6 +39,22 @@ import { MailAdapter } from 'src/shared/adapters/mail'; }; }, }), + S3Module.registerAsync({ + inject: [ConfigService], + global: true, + useFactory: (cfg: ConfigService) => ({ + connection: { + bucket: cfg.getOrThrow('S3_BUCKET_NAME'), + endpoint: cfg.getOrThrow('S3_ENDPOINT'), + credentials: { + accessKeyId: cfg.getOrThrow('S3_ACCESS_KEY'), + secretAccessKey: cfg.getOrThrow('S3_SECRET_KEY'), + }, + }, + // FOR MINIO COMPARTABLE + config: { forcePathStyle: true }, + }), + }), BullModule.forRootAsync({ inject: [ConfigService], useFactory: (cfg: ConfigService) => ({ diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 60bfa17..b2a1883 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -2,12 +2,14 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes import { IUserRepository } from './repository/user.repository.interface'; import { UpdateNotificationsDto, UpdateProfileDto } from './dtos'; import { createId } from '@paralleldrive/cuid2'; +import { S3Service } from '@libs/s3'; @Injectable() export class UserService { constructor( @Inject('IUserRepository') private readonly userRepo: IUserRepository, + private readonly s3: S3Service, ) {} private throwUserNotFound() { diff --git a/tsconfig.json b/tsconfig.json index 759daf0..e10c6c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,63 +1,43 @@ { - "compilerOptions": { - "module": "commonjs", - "declaration": false, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": false, - "noImplicitAny": false, - "strictBindCallApply": false, - "forceConsistentCasingInFileNames": false, - "noFallthroughCasesInSwitch": false, - "types": [ - "node", - "vitest/globals" - ], - "paths": { - "@libs/bootstrap": [ - "./libs/bootstrap/src" - ], - "@libs/bootstrap/*": [ - "./libs/bootstrap/src/*" - ], - "@libs/config": [ - "./libs/config/src" - ], - "@libs/config/*": [ - "./libs/config/src/*" - ], - "@libs/database": [ - "./libs/database/src" - ], - "@libs/database/*": [ - "./libs/database/src/*" - ], - "@libs/health": [ - "libs/health/src" - ], - "@libs/health/*": [ - "libs/health/src/*" - ] + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "types": ["node", "vitest/globals"], + "paths": { + "@libs/bootstrap": ["./libs/bootstrap/src"], + "@libs/bootstrap/*": ["./libs/bootstrap/src/*"], + "@libs/config": ["./libs/config/src"], + "@libs/config/*": ["./libs/config/src/*"], + "@libs/database": ["./libs/database/src"], + "@libs/database/*": ["./libs/database/src/*"], + "@libs/health": ["libs/health/src"], + "@libs/health/*": ["libs/health/src/*"], + "@libs/s3": ["libs/s3/src"], + "@libs/s3/*": ["libs/s3/src/*"] + }, + "baseUrl": "./" }, - "baseUrl": "./" - }, - "include": [ - "src/**/*", - "libs/**/*", - "test/**/*", - "drizzle.config.ts", - "vitest.config.ts", - "vitest.config.e2e.ts" - ], - "exclude": [ - "dist", - "node_modules" - ] -} \ No newline at end of file + "include": [ + "src/**/*", + "libs/**/*", + "test/**/*", + "drizzle.config.ts", + "vitest.config.ts", + "vitest.config.e2e.ts" + ], + "exclude": ["dist", "node_modules"] +} From dabedca7652559bab4a7b86ac68eb5295d8c4d0c Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 22:17:08 +0300 Subject: [PATCH 43/47] refactor(auth): fix type inconsistencies in user models --- src/modules/app/app.controller.ts | 0 .../auth/controller/auth.controller.ts | 1 - src/modules/auth/controller/auth.swagger.ts | 27 ++--- src/modules/auth/services/auth.service.ts | 19 ++- src/modules/user/commands/find-one.command.ts | 6 +- .../user/commands/update-pass.command.ts | 4 +- src/modules/user/controller/user.swagger.ts | 29 +---- src/modules/user/dtos/user.dto.ts | 50 +++++--- src/modules/user/entities/user.domain.ts | 14 ++- .../repository/user.repository.interface.ts | 25 ++-- .../user/repository/user.repository.ts | 107 +++++++--------- src/modules/user/user.service.ts | 114 ++++++++++++------ src/shared/dtos/index.ts | 1 + src/shared/dtos/response.dto.ts | 9 ++ tsconfig.json | 8 +- 15 files changed, 226 insertions(+), 188 deletions(-) create mode 100644 src/modules/app/app.controller.ts create mode 100644 src/shared/dtos/response.dto.ts diff --git a/src/modules/app/app.controller.ts b/src/modules/app/app.controller.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 1856b8e..66021be 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -31,7 +31,6 @@ export class AuthController { } @Post('sign-up/confirm') - @PostRegisterSwagger() @HttpCode(201) async verify( @Res({ passthrough: true }) res: FastifyReply, diff --git a/src/modules/auth/controller/auth.swagger.ts b/src/modules/auth/controller/auth.swagger.ts index 5b8d311..77beaef 100644 --- a/src/modules/auth/controller/auth.swagger.ts +++ b/src/modules/auth/controller/auth.swagger.ts @@ -9,6 +9,7 @@ import { ApiValidationError, } from 'src/shared/error'; import { ChangePasswordDto, Confirm2FaDto, Disable2FaDto, SignInDto, SignUpDto } from '../dtos'; +import { ActionResponse } from 'src/shared/dtos'; export const PostRegisterSwagger = () => applyDecorators( @@ -20,12 +21,7 @@ export const PostRegisterSwagger = () => ApiResponse({ status: 201, description: 'Пользователь успешно зарегистрирован.', - schema: { - example: { - success: true, - message: 'Регистрация прошла успешно', - }, - }, + type: ActionResponse.Output, }), ApiValidationError('Ошибка валидации данных (например, неверный формат email)'), ApiConflict('Пользователь с таким email уже существует'), @@ -45,9 +41,8 @@ export const PostLoginSwagger = () => schema: { example: { success: true, - require2fa: false, - accessToken: 'eyJhbGciOiJIUzI1NiIsInR5c...', - refreshToken: 'def50200508a1768c7e...', + message: false, + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', }, }, }), @@ -67,8 +62,8 @@ export const PostRefreshSwagger = () => schema: { example: { success: true, - accessToken: 'eyJhbGciOiJIUzI1NiIsInR5c...', - refreshToken: 'def50200508a1768c7e...', + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', + message: 'def50200508a1768c7e...', }, }, }), @@ -82,7 +77,7 @@ export const PostLogoutSwagger = () => summary: 'Выход из системы', description: 'Удаляет текущую сессию пользователя из Redis.', }), - ApiResponse({ status: 200, description: 'Успешный выход.' }), + ApiResponse({ status: 200, description: 'Успешный выход.', type: ActionResponse.Output }), ApiUnauthorized(), ); @@ -110,7 +105,7 @@ export const GetSessionsSwagger = () => ApiUnauthorized(), ); -export const DeleteTerminateSessionSwagger = () => +export const DeleteSessionSwagger = () => applyDecorators( ApiOperation({ summary: 'Завершить чужую сессию', @@ -129,7 +124,7 @@ export const PostChangePasswordSwagger = () => summary: 'Смена пароля', description: 'Требует текущий и новый пароль. Инвалидирует все остальные сессии.', }), - ApiBody({ type: ChangePasswordDto }), + ApiBody({ type: ChangePasswordDto.Output }), ApiResponse({ status: 200, description: 'Пароль успешно изменен.' }), ApiBadRequest('Неверный старый пароль'), ApiUnauthorized(), @@ -161,7 +156,7 @@ export const PostDisable2faSwagger = () => summary: 'Подтверждение включения 2FA', description: 'Проверяет первый код из приложения для окончательной активации 2FA.', }), - ApiBody({ type: Confirm2FaDto }), + ApiBody({ type: Confirm2FaDto.Output }), ApiResponse({ status: 200, description: 'Двухфакторная аутентификация успешно включена.' }), ApiBadRequest('Неверный код подтверждения'), ApiUnauthorized(), @@ -174,7 +169,7 @@ export const PostConfirm2faSwagger = () => description: 'Отключает двухфакторную аутентификацию (требует подтверждения паролем или текущим кодом).', }), - ApiBody({ type: Disable2FaDto }), + ApiBody({ type: Disable2FaDto.Output }), ApiResponse({ status: 200, description: '2FA успешно отключена.' }), ApiBadRequest('Неверный код или пароль для отключения'), ApiUnauthorized(), diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index d869b4b..093b1f3 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -4,6 +4,7 @@ import { ForbiddenException, Inject, Injectable, + InternalServerErrorException, NotFoundException, UnauthorizedException, UnprocessableEntityException, @@ -153,16 +154,16 @@ export class AuthService { }; public signIn = async (dto: SignInDto, meta: DeviceMetadata) => { - const user = await this.findUserCommand.execute({ email: dto.email }); + const { user, security } = await this.findUserCommand.execute({ email: dto.email }); - if (!user) { + if (!user || !security) { throw new UnauthorizedException({ code: 'INVALID_CREDENTIALS', message: 'Неверный email или пароль', }); } - const isPasswordValid = await argon.verify(user.passwordHash, dto.password); + const isPasswordValid = await argon.verify(security.passwordHash, dto.password); if (!isPasswordValid) { throw new UnauthorizedException({ @@ -208,7 +209,7 @@ export class AuthService { }); } - const user = await this.findUserCommand.execute({ id: session.userId }); + const { user } = await this.findUserCommand.execute({ id: session.userId }); if (!user) { await this.sessionRepo.revoke(session.id); @@ -267,7 +268,7 @@ export class AuthService { }); } - const user = await this.findUserCommand.execute({ email: dto.email }); + const { user } = await this.findUserCommand.execute({ email: dto.email }); if (!user) { throw new NotFoundException({ @@ -370,8 +371,14 @@ export class AuthService { } const hashed = await argon.hash(dto.password); + const isUpdated = await this.updateUserPass.execute(dto.email, hashed); - await this.updateUserPass.execute(dto.email, hashed); + if (!isUpdated) { + throw new InternalServerErrorException({ + code: 'PASSWORD_UPDATE_FAILED', + message: 'Не удалось обновить пароль. Попробуйте позже.', + }); + } await this.redis.del(redisKey); return { diff --git a/src/modules/user/commands/find-one.command.ts b/src/modules/user/commands/find-one.command.ts index efa6682..1e44d15 100644 --- a/src/modules/user/commands/find-one.command.ts +++ b/src/modules/user/commands/find-one.command.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { IUserRepository } from '../repository/user.repository.interface'; -import { User, UserWithPassword } from '../entities/user.domain'; +import type { UserWithSecurity } from '../entities/user.domain'; @Injectable() export class FindOneUserCommand { @@ -9,8 +9,8 @@ export class FindOneUserCommand { private readonly repository: IUserRepository, ) {} - async execute(params: { email: string }): Promise; - async execute(params: { id: string }): Promise; + async execute(params: { email: string }): Promise; + async execute(params: { id: string }): Promise; async execute(params: { email?: string; id?: string }): Promise { const { email, id } = params; diff --git a/src/modules/user/commands/update-pass.command.ts b/src/modules/user/commands/update-pass.command.ts index 0dd04f2..3ad7228 100644 --- a/src/modules/user/commands/update-pass.command.ts +++ b/src/modules/user/commands/update-pass.command.ts @@ -9,7 +9,7 @@ export class UpdatePassUserCommand { ) {} async execute(email: string, password: string) { - const user = await this.repository.findByEmail(email); + const { user } = await this.repository.findByEmail(email); if (!user) { throw new NotFoundException({ @@ -19,6 +19,6 @@ export class UpdatePassUserCommand { }); } - await this.repository.updatePasswordHash(user.id, password); + return this.repository.updatePasswordHash(user.id, password); } } diff --git a/src/modules/user/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts index d89723b..423699c 100644 --- a/src/modules/user/controller/user.swagger.ts +++ b/src/modules/user/controller/user.swagger.ts @@ -9,6 +9,7 @@ import { import { UpdateNotificationsDto, UpdateProfileDto, UserResponse } from '../dtos'; import { applyDecorators } from '@nestjs/common'; import { ApiBadRequest, ApiUnauthorized, ApiValidationError } from 'src/shared/error'; +import { ActionResponse } from 'src/shared/dtos'; export const GetMeSwagger = () => applyDecorators( @@ -36,17 +37,7 @@ export const PatchMeSwagger = () => ApiResponse({ status: 200, description: 'Профиль успешно обновлен.', - schema: { - example: { - success: true, - message: 'Профиль успешно обновлен.', - updatedAt: '2026-10-24T14:30:00.000Z', - data: { - fullName: 'Alexey Smirnov', - timezone: 'Europe/Moscow', - }, - }, - }, + type: ActionResponse.Output, }), ApiValidationError('Ошибка валидации (например, слишком короткое имя)', [ { @@ -70,14 +61,7 @@ export const PatchMeNotificationsSwagger = () => ApiResponse({ status: 200, description: 'Настройки успешно сохранены.', - schema: { - example: { - success: true, - newSettings: { - email: { task_assigned: false }, - }, - }, - }, + type: ActionResponse.Output, }), ApiValidationError('Некорректный формат настроек'), ApiUnauthorized(), @@ -143,12 +127,7 @@ export const PostMeAvatarSwagger = () => ApiResponse({ status: 201, description: 'Аватар успешно загружен.', - schema: { - example: { - avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', - success: true, - }, - }, + type: ActionResponse.Output, }), ApiBadRequest('Файл не передан или имеет неверный формат'), ApiUnauthorized(), diff --git a/src/modules/user/dtos/user.dto.ts b/src/modules/user/dtos/user.dto.ts index 5d47ec7..d342b79 100644 --- a/src/modules/user/dtos/user.dto.ts +++ b/src/modules/user/dtos/user.dto.ts @@ -28,26 +28,48 @@ const SecuritySchema = z }) .describe('Данные безопасности аккаунта'); +const ProfileSchema = z.object({ + firstName: z.string().describe('Имя пользователя'), + lastName: z.string().describe('Фамилия'), + middleName: z.string().nullable().describe('Отчество'), + bio: z.string().nullable().describe('О себе'), + avatarUrl: z.string().url().nullable().describe('Ссылка на аватар в S3'), + timezone: z.string().describe('Временная зона'), + language: z.string().describe('Язык интерфейса'), + createdAt: z.string().datetime().describe('Дата регистрации'), + updatedAt: z.string().datetime().describe('Дата последнего обновления профиля'), +}); + export const UserSchema = z.object({ - id: z.string().cuid2().describe('Уникальный идентификатор пользователя (CUID2)'), - fullName: z.string().min(2).max(255).describe('Полное имя пользователя'), + id: z.string().describe('Уникальный идентификатор (CUID/UUID)'), email: z.string().email().describe('Электронная почта'), - bio: z.string().max(1000).nullable().describe('Информация "О себе"'), - avatarUrl: z.string().url().describe('Ссылка на аватарку пользователя'), - timezone: z.string().describe('Временная зона пользователя (IANA формат)'), - language: z.enum(['ru', 'en']).describe('Выбранный язык интерфейса'), + profile: ProfileSchema, security: SecuritySchema, notifications: NotificationsSchema, }); + export class UserResponse extends createZodDto(UserSchema) {} -export const UpdateProfileSchema = UserSchema.pick({ - fullName: true, - bio: true, - timezone: true, - language: true, -}) - .partial() - .describe('Схема для частичного обновления профиля'); +export const UpdateProfileSchema = z + .object({ + firstName: z + .string() + .min(1, 'Имя не может быть пустым') + .max(50, 'Имя слишком длинное') + .optional(), + lastName: z + .string() + .min(1, 'Фамилия не может быть пустой') + .max(50, 'Фамилия слишком длинная') + .optional(), + middleName: z.string().max(50, 'Отчество слишком длинное').nullish(), + bio: z.string().max(1000, 'О себе не более 1000 символов').nullish(), + timezone: z.string().max(50).optional(), + language: z + .string() + .length(2, 'Используйте формат ISO (например, "ru" или "en")') + .optional(), + }) + .describe('Схема для частичного обновления данных профиля'); export class UpdateProfileDto extends createZodDto(UpdateProfileSchema) {} diff --git a/src/modules/user/entities/user.domain.ts b/src/modules/user/entities/user.domain.ts index edb7e34..0721065 100644 --- a/src/modules/user/entities/user.domain.ts +++ b/src/modules/user/entities/user.domain.ts @@ -8,14 +8,18 @@ export type UserSecurity = InferSelectModel; export type NewUserSecurity = InferInsertModel; export type UserNotifications = InferSelectModel; -export type NotificationSettings = UserNotifications['settings']; +export type NotificationSettings = Pick; export type UserActivity = InferSelectModel; export type NewUserActivity = InferInsertModel; -export type UserProfile = User & { - security: Omit; - notifications: UserNotifications['settings']; +export type UserProfile = { + user: User; + security: Pick; + notifications: NotificationSettings['settings']; }; -export type UserWithPassword = User & UserSecurity; +export type UserWithSecurity = { + user: User; + security: Pick; +}; diff --git a/src/modules/user/repository/user.repository.interface.ts b/src/modules/user/repository/user.repository.interface.ts index 661e25a..e2c4bbe 100644 --- a/src/modules/user/repository/user.repository.interface.ts +++ b/src/modules/user/repository/user.repository.interface.ts @@ -5,22 +5,14 @@ import type { UserActivity, UserNotifications, UserProfile, - UserWithPassword, + UserWithSecurity, } from '../entities/user.domain'; export interface IUserRepository { - findById(id: string): Promise; - findByEmail(email: string): Promise; - - existsByEmail(email: string): Promise; - - updateProfile(id: string, data: Partial): Promise; - updateNotifications(id: string, settings: UserNotifications['settings']): Promise; - updateAvatar(id: string, url: string): Promise; - create(data: NewUser): Promise; - - logActivity(data: NewUserActivity): Promise; + findById(id: string): Promise; + findByEmail(email: string): Promise; + findProfile(id: string): Promise; findActivityByUser( userId: string, options: { limit: number; offset: number }, @@ -28,8 +20,9 @@ export interface IUserRepository { items: UserActivity[]; total: number; }>; - - findProfile(id: string): Promise; - - updatePasswordHash(id: string, hash: string): Promise; + updateAvatar(id: string, url: string): Promise; + updateProfile(id: string, data: Partial): Promise; + updatePasswordHash(id: string, hash: string): Promise; + updateNotifications(id: string, settings: UserNotifications['settings']): Promise; + logActivity(data: NewUserActivity): Promise; } diff --git a/src/modules/user/repository/user.repository.ts b/src/modules/user/repository/user.repository.ts index d3c5595..757958b 100644 --- a/src/modules/user/repository/user.repository.ts +++ b/src/modules/user/repository/user.repository.ts @@ -2,13 +2,7 @@ import * as sc from '../entities'; import { DATABASE_SERVICE, DatabaseService } from '@libs/database'; import { IUserRepository } from './user.repository.interface'; import { Inject, Injectable } from '@nestjs/common'; -import type { - NewUser, - NewUserActivity, - User, - UserNotifications, - UserWithPassword, -} from '../entities/user.domain'; +import type { NewUser, NewUserActivity, User, UserNotifications } from '../entities/user.domain'; import { createId } from '@paralleldrive/cuid2'; import { desc, eq, count } from 'drizzle-orm'; @@ -16,66 +10,62 @@ import { desc, eq, count } from 'drizzle-orm'; export class UserRepository implements IUserRepository { constructor( @Inject(DATABASE_SERVICE) - private readonly repository: DatabaseService, + private readonly db: DatabaseService, ) {} - async findProfile(id: string) { - const rows = await this.repository + private get fullUserQuery() { + return this.db .select() .from(sc.users) .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) - .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)) - .where(eq(sc.users.id, id)); + .leftJoin(sc.userNotifications, eq(sc.users.id, sc.userNotifications.userId)); + } - if (rows.length === 0) return null; + async findProfile(id: string) { + const [rows] = await this.fullUserQuery.where(eq(sc.users.id, id)); + if (!rows.users) return null; + const { lastPasswordChange, is2faEnabled } = rows.user_security; + const { settings } = rows.user_notifications; - const { users: user, user_security: security, user_notifications: notifications } = rows[0]; + return { + user: rows.users, + security: { lastPasswordChange, is2faEnabled }, + notifications: settings, + }; + } + async findById(id: string) { + const [row] = await this.fullUserQuery.where(eq(sc.users.id, id)); + if (!row || !row.user_security) return null; return { - ...user, + user: row.users, security: { - is2faEnabled: security?.is2faEnabled ?? false, - lastPasswordChange: security?.lastPasswordChange ?? user.createdAt, - }, - notifications: notifications?.settings ?? { - email: { task_assigned: true, mentions: true, daily_summary: false }, - push: { task_assigned: true, reminders: true }, + passwordHash: row.user_security.passwordHash, }, }; } - async findById(id: string): Promise { - const [result] = await this.repository.select().from(sc.users).where(eq(sc.users.id, id)); - return result || null; - } - - async findByEmail(email: string): Promise { - const [result] = await this.repository - .select() - .from(sc.users) - .leftJoin(sc.userSecurity, eq(sc.users.id, sc.userSecurity.userId)) - .where(eq(sc.users.email, email)); - - if (!result || !result.users) { - return null; - } - + async findByEmail(email: string) { + const [row] = await this.fullUserQuery.where(eq(sc.users.email, email.toLowerCase())); + if (!row || !row.user_security) return null; return { - ...result.users, - ...result.user_security, + user: row.users, + security: { + passwordHash: row.user_security.passwordHash, + }, }; } async findSecurityByUserId(userId: string) { - const [result] = await this.repository + const [result] = await this.db .select() .from(sc.userSecurity) .where(eq(sc.userSecurity.userId, userId)); return result || null; } - async create(data: NewUser): Promise { - return await this.repository.transaction(async (tx) => { + async create(data: NewUser) { + return await this.db.transaction(async (tx) => { const [newUser] = await tx.insert(sc.users).values(data).returning(); await tx.insert(sc.userNotifications).values({ @@ -86,61 +76,56 @@ export class UserRepository implements IUserRepository { }); } - async existsByEmail(email: string): Promise { - const [result] = await this.repository - .select({ value: count() }) - .from(sc.users) - .where(eq(sc.users.email, email)); - return (result?.value ?? 0) > 0; - } - - async updateProfile(id: string, data: Partial): Promise { - const [updated] = await this.repository + async updateProfile(id: string, data: Partial) { + const { rowCount } = await this.db .update(sc.users) .set({ ...data, updatedAt: new Date() }) - .where(eq(sc.users.id, id)) - .returning(); - return updated; + .where(eq(sc.users.id, id)); + return (rowCount ?? 0) > 0; } async updateNotifications(id: string, settings: UserNotifications['settings']) { - await this.repository + const { rowCount } = await this.db .update(sc.userNotifications) .set({ settings }) .where(eq(sc.userNotifications.userId, id)); + return (rowCount ?? 0) > 0; } async updateAvatar(id: string, url: string) { - await this.repository + const { rowCount } = await this.db .update(sc.users) .set({ avatarUrl: url, updatedAt: new Date() }) .where(eq(sc.users.id, id)); + return (rowCount ?? 0) > 0; } async updatePasswordHash(id: string, hash: string) { - await this.repository + const { rowCount } = await this.db .insert(sc.userSecurity) .values({ userId: id, passwordHash: hash }) .onConflictDoUpdate({ target: sc.userSecurity.userId, set: { passwordHash: hash, lastPasswordChange: new Date() }, }); + return (rowCount ?? 0) > 0; } async logActivity(data: NewUserActivity) { - await this.repository.insert(sc.userActivity).values({ + const { rowCount } = await this.db.insert(sc.userActivity).values({ ...data, id: data.id ?? createId(), }); + return (rowCount ?? 0) > 0; } async findActivityByUser(userId: string, options: { limit: number; offset: number }) { const [totalResult, items] = await Promise.all([ - this.repository + this.db .select({ value: count() }) .from(sc.userActivity) .where(eq(sc.userActivity.userId, userId)), - this.repository + this.db .select() .from(sc.userActivity) .where(eq(sc.userActivity.userId, userId)) diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index b2a1883..78934f1 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,4 +1,10 @@ -import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; import { IUserRepository } from './repository/user.repository.interface'; import { UpdateNotificationsDto, UpdateProfileDto } from './dtos'; import { createId } from '@paralleldrive/cuid2'; @@ -19,53 +25,91 @@ export class UserService { }); } - public getProfile = async (id: string) => { - const user = await this.userRepo.findProfile(id); + public getProfile = async (userId: string) => { + const { user, notifications, security } = await this.userRepo.findProfile(userId); if (!user) this.throwUserNotFound(); - return user; + const { id, email, ...profile } = user; + + return { + id, + email, + profile, + security, + notifications, + }; }; public updateProfile = async (id: string, dto: UpdateProfileDto) => { - const user = await this.userRepo.findById(id); - if (!user) this.throwUserNotFound(); - - const updatedUser = await this.userRepo.updateProfile(id, dto); + const keysToUpdate = Object.keys(dto); + if (keysToUpdate.length === 0) { + return { + success: true, + message: 'Изменений не обнаружено', + }; + } - await this.userRepo.logActivity({ - id: createId(), - userId: id, - eventType: 'PROFILE_UPDATED', - metadata: { fields: Object.keys(dto) }, - }); + try { + const isUpdated = await this.userRepo.updateProfile(id, dto); + + if (!isUpdated) { + throw new InternalServerErrorException('Не удалось обновить профиль'); + } + + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'PROFILE_UPDATED', + metadata: { + fields: keysToUpdate, + }, + }); - return { - success: true, - message: 'Профиль успешно обновлен', - updatedAt: new Date().toISOString(), - data: updatedUser, - }; + return { + success: true, + message: 'Профиль успешно обновлен', + }; + } catch (error) { + throw error; + } }; public updateNotifications = async (id: string, dto: UpdateNotificationsDto) => { - const user = await this.userRepo.findById(id); + const keysToUpdate = Object.keys(dto); + if (keysToUpdate.length === 0) { + return { + success: true, + message: 'Изменений не обнаружено', + }; + } + const user = await this.userRepo.findById(id); if (!user) this.throwUserNotFound(); - const settings = await this.userRepo.updateNotifications(id, { - email: dto.email, - push: dto.push, - }); + try { + const isUpdated = await this.userRepo.updateNotifications(id, { + email: dto.email, + push: dto.push, + }); - await this.userRepo.logActivity({ - id: createId(), - userId: id, - eventType: 'NOTIFICATIONS_UPDATED', - }); + if (!isUpdated) { + throw new InternalServerErrorException( + 'Ошибка при сохранении настроек уведомлений', + ); + } - return { - success: true, - newSettings: settings, - }; + await this.userRepo.logActivity({ + id: createId(), + userId: id, + eventType: 'NOTIFICATIONS_UPDATED', + }); + + return { + success: true, + message: 'Настройки уведомлений обновлены', + }; + } catch (error) { + throw error; + } }; public getActivity = async (id: string, page: number, limit: number) => { @@ -107,6 +151,6 @@ export class UserService { metadata: { url: avatarUrl }, }); - return { avatarUrl, success: true }; + return { success: true, message: '' }; }; } diff --git a/src/shared/dtos/index.ts b/src/shared/dtos/index.ts index 5f10edb..5a8e94b 100644 --- a/src/shared/dtos/index.ts +++ b/src/shared/dtos/index.ts @@ -1 +1,2 @@ export * from './pagination.dto'; +export * from './response.dto'; diff --git a/src/shared/dtos/response.dto.ts b/src/shared/dtos/response.dto.ts new file mode 100644 index 0000000..325f719 --- /dev/null +++ b/src/shared/dtos/response.dto.ts @@ -0,0 +1,9 @@ +import { z } from 'zod/v4'; +import { createZodDto } from 'nestjs-zod'; + +export const ActionResponseSchema = z.object({ + success: z.boolean().describe('Статус операции'), + message: z.string().optional().describe('Сообщение для пользователя'), +}); + +export class ActionResponse extends createZodDto(ActionResponseSchema) {} diff --git a/tsconfig.json b/tsconfig.json index e10c6c1..4f21469 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,10 +24,10 @@ "@libs/config/*": ["./libs/config/src/*"], "@libs/database": ["./libs/database/src"], "@libs/database/*": ["./libs/database/src/*"], - "@libs/health": ["libs/health/src"], - "@libs/health/*": ["libs/health/src/*"], - "@libs/s3": ["libs/s3/src"], - "@libs/s3/*": ["libs/s3/src/*"] + "@libs/health": ["./libs/health/src"], + "@libs/health/*": ["./libs/health/src/*"], + "@libs/s3": ["./libs/s3/src"], + "@libs/s3/*": ["./libs/s3/src/*"] }, "baseUrl": "./" }, From 4f216dbc9a3d4674f79148ec4452d3aa8f480d82 Mon Sep 17 00:00:00 2001 From: soorq Date: Sun, 12 Apr 2026 22:50:38 +0300 Subject: [PATCH 44/47] feat(infra): setup migrations, optimize pg pool, and add ghcr workflow --- .env.example | 4 +-- .github/workflows/build.yml | 19 ++++++----- Dockerfile.prod | 2 ++ infra/README.md | 2 +- infra/dev/README.md | 41 +++++++++++++++++++++++ infra/{ => dev}/compose.dev.yaml | 32 +++++++++++++----- libs/config/src/config.schema.ts | 2 +- libs/database/src/database.module.ts | 12 ++++--- src/modules/app/app.controller.ts | 0 src/modules/app/app.module.ts | 4 ++- src/modules/auth/auth.module.ts | 7 ++-- src/shared/migration/index.ts | 1 + src/shared/migration/migration.service.ts | 27 +++++++++++++++ 13 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 infra/dev/README.md rename infra/{ => dev}/compose.dev.yaml (65%) delete mode 100644 src/modules/app/app.controller.ts create mode 100644 src/shared/migration/index.ts create mode 100644 src/shared/migration/migration.service.ts diff --git a/.env.example b/.env.example index 1014c59..7421df7 100644 --- a/.env.example +++ b/.env.example @@ -18,10 +18,10 @@ DB_SCHEMA=base DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} # --- REDIS --- -# in the docker network will be +# in the docker network will be, not show port redis, at prod env # REDIS_HOST=redis # at development mode -REDIS_HOST=redis +REDIS_HOST=127.0.0.1 REDIS_PORT=7000 JWT_ACCESS_SECRET=same-same-same-same-same diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b1744f..f1a92a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build and Push on: push: - branches: [dev, main] + branches: [dev, main, feat/**] env: REGISTRY: ghcr.io @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - # packages: write + packages: write steps: - uses: actions/checkout@v4 @@ -21,12 +21,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - # - name: Log in to the Container registry - # uses: docker/login-action@v3 - # with: - # registry: ${{ env.REGISTRY }} - # username: ${{ github.actor }} - # password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) id: meta @@ -36,13 +36,14 @@ jobs: tags: | type=ref,event=branch type=sha,format=short + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile.prod - push: false # add true, if your setup variables + push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/Dockerfile.prod b/Dockerfile.prod index 7112e09..b0ac8e7 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -28,7 +28,9 @@ ENV PORT=3000 COPY --from=build /app/dist ./dist COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/migrations ./migrations COPY --from=build /app/package.json ./ +COPY --from=build /app/drizzle.config.ts ./drizzle.config.ts EXPOSE 3000 diff --git a/infra/README.md b/infra/README.md index 310228c..57e9bf6 100644 --- a/infra/README.md +++ b/infra/README.md @@ -3,5 +3,5 @@ Run it by pwd at root! Not include at this dir ```sh -docker compose -f .\infra\compose.dev.yaml --env-file .env --profile infra up --build -d -V +docker compose -f ./infra/dev/compose.dev.yaml --env-file .env --profile infra up --build -d -V ``` diff --git a/infra/dev/README.md b/infra/dev/README.md new file mode 100644 index 0000000..ac46b3a --- /dev/null +++ b/infra/dev/README.md @@ -0,0 +1,41 @@ +# Файл для фронт разрабов + +## Описание + +Данный конфиг разворачивает полный инстанс бэкенда (API + DB + Redis) +для локальной разработки фронтенда. + +## ТРЕБОВАНИЯ: + +1. Положить актуальный файл .env в директорию с этим файлом + (путь: ./infra/dev/.env). +2. Наличие Docker Desktop / Docker Engine. + +## ЗАПУСК: + +Выполните команду из корня проекта: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra up --pull always --build -d -V +``` + +## ЧТО ВНУТРИ: + +- API: http://localhost:3000 +- Postgres: localhost:6000 (пароли и база берутся из .env) +- Redis: localhost:7000 + +## ОСОБЕННОСТИ: + +- Авто-миграции: Приложение само накатит SQL-схему при старте. +- Healthchecks: Контейнер API не поднимется, пока DB и Redis + не станут доступны (status: healthy). +- Изоляция: Используется выделенная сеть 'task-tracker-gateway'. + +## RESET: + +Если нужно полностью очистить базу и начать с нуля: + +```sh +docker compose -f ./infra/dev/compose.dev.yaml --profile infra down -v +``` diff --git a/infra/compose.dev.yaml b/infra/dev/compose.dev.yaml similarity index 65% rename from infra/compose.dev.yaml rename to infra/dev/compose.dev.yaml index 88bbec3..df523af 100644 --- a/infra/compose.dev.yaml +++ b/infra/dev/compose.dev.yaml @@ -1,24 +1,31 @@ version: "3.9" -name: task-tracker +name: task-tracker-api services: api: hostname: api container_name: api - build: - context: ../ - dockerfile: Dockerfile.dev - restart: always + image: ghcr.io/task-tracker-lab/task-tracker-backend:feat-user env_file: - - ../.env + - .env ports: - "3000:3000" depends_on: database: condition: service_healthy + redis: + condition: service_healthy networks: - backend + deploy: + resources: + limits: + cpus: "2.0" + memory: 1024M + reservations: + cpus: "0.5" + memory: 256M database: hostname: database @@ -26,7 +33,7 @@ services: image: postgres:16-alpine restart: always env_file: - - ../.env + - .env environment: POSTGRES_USER: ${DB_USERNAME} POSTGRES_PASSWORD: ${DB_PASSWORD} @@ -38,7 +45,11 @@ services: networks: - backend healthcheck: - test: ["CMD-SHELL", "pg_isready -U \"$$POSTGRES_USER\" -d \"$$POSTGRES_DB\" -q || exit 1"] + test: + [ + "CMD-SHELL", + 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -q || exit 1', + ] interval: 5s timeout: 5s retries: 5 @@ -56,6 +67,11 @@ services: - redis_data:/data networks: - backend + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 profiles: ["infra"] volumes: diff --git a/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 1f72630..e28d54f 100644 --- a/libs/config/src/config.schema.ts +++ b/libs/config/src/config.schema.ts @@ -14,7 +14,7 @@ export const ConfigSchema = z.object({ DB_SCHEMA: z.string({ error: 'DB_SCHEMA is missing' }), DATABASE_URL: z.string().url('DATABASE_URL must be a valid connection string'), REDIS_HOST: z.string().default('redis'), - REDIS_PORT: z.coerce.number().default(6379), + REDIS_PORT: z.coerce.number().optional().default(6379), DOMAIN: z .string() .toLowerCase() diff --git a/libs/database/src/database.module.ts b/libs/database/src/database.module.ts index 7a89484..07d5c78 100644 --- a/libs/database/src/database.module.ts +++ b/libs/database/src/database.module.ts @@ -55,15 +55,17 @@ export class DatabaseModule implements OnApplicationShutdown { provide: DATABASE_SERVICE, useFactory: async (cfg: ConfigService, opts: DatabaseModuleOptions) => { const baseUrl = cfg.get('DATABASE_URL'); + const url = new URL(baseUrl); + url.searchParams.set('options', `-c search_path=${opts.schemaName || 'public'}`); const pool = new Pool({ - connectionString: baseUrl, + connectionString: url.toString(), max: 20, + min: 5, + connectionTimeoutMillis: 5000, idleTimeoutMillis: 30000, - }); - - pool.on('connect', (client) => { - client.query(`SET search_path TO ${opts.schemaName || 'public'}`); + maxUses: 7500, + keepAlive: true, }); this.pool = pool; diff --git a/src/modules/app/app.controller.ts b/src/modules/app/app.controller.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index ed9676d..3ddc308 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -16,6 +16,7 @@ import { MailProcessor } from 'src/shared/workers'; import { BullModule } from '@nestjs/bullmq'; import { MailAdapter } from 'src/shared/adapters/mail'; import { S3Module } from '@libs/s3'; +import { MigrationService } from 'src/shared/migration'; @Module({ imports: [ @@ -60,7 +61,7 @@ import { S3Module } from '@libs/s3'; useFactory: (cfg: ConfigService) => ({ connection: { host: cfg.getOrThrow('REDIS_HOST'), - port: cfg.getOrThrow('REDIS_PORT'), + port: cfg.get('REDIS_PORT'), }, }), }), @@ -73,6 +74,7 @@ import { S3Module } from '@libs/s3'; HealthModule.register('gateway'), ], providers: [ + MigrationService, { provide: 'IMailPort', useClass: MailAdapter, diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 14fe3ed..1b8555d 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -37,12 +37,13 @@ import { BullMQAdapter } from '@bull-board/api/bullMQAdapter'; RedisModule.forRootAsync({ inject: [ConfigService], useFactory: async (cfg: ConfigService) => { - const host = cfg.get('REDIS_HOST', { infer: true }); - const port = cfg.get('REDIS_PORT', { infer: true }); + const host = cfg.getOrThrow('REDIS_HOST', { infer: true }); + const port = cfg.get('REDIS_PORT'); + const url = `redis://${host}${port ? `:${port}` : ''}`; return { type: 'single', - url: `redis://${host}:${port}`, + url, options: { retryStrategy(times) { return Math.min(times * 50, 2000); diff --git a/src/shared/migration/index.ts b/src/shared/migration/index.ts new file mode 100644 index 0000000..1be4fe2 --- /dev/null +++ b/src/shared/migration/index.ts @@ -0,0 +1 @@ +export { MigrationService } from './migration.service'; diff --git a/src/shared/migration/migration.service.ts b/src/shared/migration/migration.service.ts new file mode 100644 index 0000000..e1e49f9 --- /dev/null +++ b/src/shared/migration/migration.service.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable, OnModuleInit, Logger } from '@nestjs/common'; +import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { DATABASE_SERVICE, type DatabaseService } from '@libs/database'; +import * as path from 'path'; + +@Injectable() +export class MigrationService implements OnModuleInit { + private readonly logger = new Logger(MigrationService.name); + + constructor( + @Inject(DATABASE_SERVICE) + private readonly db: DatabaseService>, + ) {} + + async onModuleInit() { + this.logger.debug('Checking for database migrations...'); + try { + await migrate(this.db, { + migrationsFolder: path.resolve(process.cwd(), 'migrations'), + }); + this.logger.debug('Migrations completed or already up to date'); + } catch (error) { + this.logger.error('Migration failed', error); + process.exit(1); + } + } +} From 9b20271af1e6727f83405fb21de192419bc293c7 Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 13 Apr 2026 03:34:01 +0300 Subject: [PATCH 45/47] chore(auth): add password reset and sign-up confirmation Swagger documentation --- .../auth/controller/auth.controller.ts | 8 ++ src/modules/auth/controller/auth.swagger.ts | 97 ++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 66021be..acb1689 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -4,8 +4,12 @@ import { AuthService } from '../services/auth.service'; import { PostLoginSwagger, PostLogoutSwagger, + PostPasswordResetConfirmSwagger, + PostPasswordResetSwagger, + PostPasswordResetVerifySwagger, PostRefreshSwagger, PostRegisterSwagger, + PostSignUpConfirmSwagger, } from './auth.swagger'; import { PasswordResetConfirmDto, @@ -31,6 +35,7 @@ export class AuthController { } @Post('sign-up/confirm') + @PostSignUpConfirmSwagger() @HttpCode(201) async verify( @Res({ passthrough: true }) res: FastifyReply, @@ -102,16 +107,19 @@ export class AuthController { } @Post('password/reset') + @PostPasswordResetSwagger() async resetPasswordRequest(@Body() dto: ResetPasswordDto) { return this.facade.resetPass(dto); } @Post('password/reset/verify') + @PostPasswordResetVerifySwagger() async verifyResetCode(@Body() dto: VerifyResetCodeDto) { return this.facade.verifyResetPassword(dto); } @Post('password/reset/confirm') + @PostPasswordResetConfirmSwagger() async confirmPasswordReset(@Body() dto: PasswordResetConfirmDto) { return this.facade.confirmResetPass(dto); } diff --git a/src/modules/auth/controller/auth.swagger.ts b/src/modules/auth/controller/auth.swagger.ts index 77beaef..49f4d72 100644 --- a/src/modules/auth/controller/auth.swagger.ts +++ b/src/modules/auth/controller/auth.swagger.ts @@ -3,12 +3,23 @@ import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ApiBadRequest, ApiConflict, + ApiErrorResponse, ApiForbidden, ApiNotFound, ApiUnauthorized, ApiValidationError, } from 'src/shared/error'; -import { ChangePasswordDto, Confirm2FaDto, Disable2FaDto, SignInDto, SignUpDto } from '../dtos'; +import { + ChangePasswordDto, + Confirm2FaDto, + Disable2FaDto, + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from '../dtos'; import { ActionResponse } from 'src/shared/dtos'; export const PostRegisterSwagger = () => @@ -81,6 +92,90 @@ export const PostLogoutSwagger = () => ApiUnauthorized(), ); +export const PostSignUpConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Подтверждение регистрации по коду', + description: + 'Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.', + }), + ApiBody({ type: VerifyDto.Output }), + ApiResponse({ + status: 201, + description: 'Аккаунт подтверждён, сессия создана.', + schema: { + example: { + success: true, + message: 'Аккаунт успешно подтвержден', + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', + }, + }, + }), + ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'), + ApiBadRequest('Срок регистрации истёк или сессия не найдена'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const PostPasswordResetSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Запрос кода восстановления пароля', + description: 'Отправляет одноразовый код на email, если пользователь существует.', + }), + ApiBody({ type: ResetPasswordDto.Output }), + ApiResponse({ + status: 201, + description: 'Код отправлен на почту (при успешной обработке запроса).', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат email'), + ApiErrorResponse( + 422, + 'INVALID_EMAIL_FORMAT', + 'Указанный email адрес имеет некорректный формат', + ), + ApiNotFound('Пользователь с таким email не найден'), + ); + +export const PostPasswordResetVerifySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Проверка кода восстановления пароля', + description: 'Проверяет код из письма и помечает сессию сброса как подтверждённую.', + }), + ApiBody({ type: VerifyResetCodeDto.Output }), + ApiResponse({ + status: 201, + description: 'Код подтверждён, можно задать новый пароль.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (email или формат кода)'), + ApiBadRequest('Время подтверждения истекло или запрос не найден'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const PostPasswordResetConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Установка нового пароля после сброса', + description: 'Доступно только после успешной проверки кода на шаге verify.', + }), + ApiBody({ type: PasswordResetConfirmDto.Output }), + ApiResponse({ + status: 201, + description: 'Пароль успешно изменён.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (пароли не совпадают или неверная длина)'), + ApiBadRequest('Сессия восстановления не найдена или истекла'), + ApiForbidden(), + ApiErrorResponse( + 500, + 'PASSWORD_UPDATE_FAILED', + 'Не удалось обновить пароль. Попробуйте позже.', + ), + ); + export const GetSessionsSwagger = () => applyDecorators( ApiOperation({ From 04b21b434394e5cd918c8c9176a64fe81235fdc0 Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 13 Apr 2026 19:25:42 +0300 Subject: [PATCH 46/47] feat(user): implement avatar upload functionality with S3 integration --- infra/dev/compose.dev.yaml | 38 +++++++++++++++++++ libs/bootstrap/src/bootstrap.ts | 7 ++++ libs/s3/src/dtos/upload-avatar.dto.ts | 5 +++ libs/s3/src/s3.service.ts | 25 ++++++++++++ package.json | 1 + pnpm-lock.yaml | 24 ++++++++++++ src/modules/app/app.module.ts | 1 + .../user/controller/user.controller.ts | 30 ++++++++++++--- src/modules/user/user.service.ts | 20 +++++++--- 9 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 libs/s3/src/dtos/upload-avatar.dto.ts diff --git a/infra/dev/compose.dev.yaml b/infra/dev/compose.dev.yaml index df523af..19e2855 100644 --- a/infra/dev/compose.dev.yaml +++ b/infra/dev/compose.dev.yaml @@ -74,9 +74,47 @@ services: retries: 5 profiles: ["infra"] + minio: + hostname: minio + container_name: minio + image: minio/minio:latest + restart: always + environment: + MINIO_ROOT_USER: ${S3_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} + ports: + - "9000:9000" # API + - "9001:9001" # Console (UI) + command: server /data --console-address ":9001" + volumes: + - minio_data:/data + networks: + - backend + profiles: [ "infra" ] + + minio-init: + image: minio/mc:latest + depends_on: + - minio + environment: + MINIO_ROOT_USER: ${S3_ACCESS_KEY} + MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY} + networks: + - backend + profiles: [ "infra" ] + entrypoint: > + /bin/sh -c " + sleep 5; + mc alias set myminio http://minio:9000 ${S3_ACCESS_KEY} ${S3_SECRET_KEY}; + mc mb myminio/${S3_BUCKET_NAME} --ignore-existing; + mc anonymous set download myminio/${S3_BUCKET_NAME}; + exit 0; + " + volumes: postgres_data: redis_data: + minio_data: networks: backend: diff --git a/libs/bootstrap/src/bootstrap.ts b/libs/bootstrap/src/bootstrap.ts index 80ec104..0ca163f 100644 --- a/libs/bootstrap/src/bootstrap.ts +++ b/libs/bootstrap/src/bootstrap.ts @@ -8,6 +8,7 @@ import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fa import type { BootstrapOptions } from './interfaces/options.interface'; import fastifyCookie from '@fastify/cookie'; import fastifyCompress from '@fastify/compress'; +import fastifyMultipart from '@fastify/multipart'; export async function bootstrapApp(options: BootstrapOptions) { const adapter = new FastifyAdapter(); @@ -47,6 +48,12 @@ export async function bootstrapApp(options: BootstrapOptions) { threshold: 1024, }); + await app.register(fastifyMultipart, { + limits: { + fileSize: 5 * 1024 * 1024, + }, + }); + if (apiPrefix) app.setGlobalPrefix(apiPrefix); if (useCors) setupCors(app, origins); if (swaggerOptions) { diff --git a/libs/s3/src/dtos/upload-avatar.dto.ts b/libs/s3/src/dtos/upload-avatar.dto.ts new file mode 100644 index 0000000..32a11f5 --- /dev/null +++ b/libs/s3/src/dtos/upload-avatar.dto.ts @@ -0,0 +1,5 @@ +export class FileUploadDto { + buffer: Buffer; + filename: string; + mimetype: string; +} diff --git a/libs/s3/src/s3.service.ts b/libs/s3/src/s3.service.ts index 3ad5182..47d8a8d 100644 --- a/libs/s3/src/s3.service.ts +++ b/libs/s3/src/s3.service.ts @@ -2,11 +2,15 @@ import { Inject, Injectable } from '@nestjs/common'; import { S3Client } from '@aws-sdk/client-s3'; import { S3_OPTIONS } from './s3.constants'; import { S3ModuleOptions } from './interfaces'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { randomUUID } from 'crypto'; +import { extname } from 'path'; @Injectable() export class S3Service { private readonly s3Client: S3Client; public readonly bucket: string; + private readonly endpoint: string; constructor( @Inject(S3_OPTIONS) @@ -14,6 +18,7 @@ export class S3Service { ) { const { bucket, credentials, endpoint, region } = options.connection; this.bucket = bucket; + this.endpoint = endpoint as string; this.s3Client = new S3Client({ region, @@ -22,4 +27,24 @@ export class S3Service { ...options.config, }); } + + async uploadPublicFile( + fileBuffer: Buffer, + originalName: string, + mimetype: string, + ): Promise { + const extension = extname(originalName); + const fileName = `${randomUUID()}${extension}`; + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: fileName, + Body: fileBuffer, + ContentType: mimetype, + }); + + await this.s3Client.send(command); + + return `${this.endpoint}/${this.bucket}/${fileName}`; + } } diff --git a/package.json b/package.json index 829c9fd..e162de6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@fastify/compress": "^8.3.1", "@fastify/cookie": "^11.0.2", "@fastify/cors": "^11.2.0", + "@fastify/multipart": "^10.0.0", "@fastify/static": "^9.1.0", "@nestjs-modules/ioredis": "^2.2.1", "@nestjs/bullmq": "^11.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec34f87..4d0faec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@fastify/cors': specifier: ^11.2.0 version: 11.2.0 + '@fastify/multipart': + specifier: ^10.0.0 + version: 10.0.0 '@fastify/static': specifier: ^9.1.0 version: 9.1.0 @@ -1049,6 +1052,9 @@ packages: '@fastify/ajv-compiler@4.0.5': resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@fastify/compress@8.3.1': resolution: {integrity: sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==} @@ -1058,6 +1064,9 @@ packages: '@fastify/cors@11.2.0': resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==} + '@fastify/deepmerge@3.2.1': + resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==} + '@fastify/error@4.2.0': resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} @@ -1073,6 +1082,9 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + '@fastify/multipart@10.0.0': + resolution: {integrity: sha512-pUx3Z1QStY7E7kwvDTIvB6P+rF5lzP+iqPgZyJyG3yBJVPvQaZxzDHYbQD89rbY0ciXrMOyGi8ezHDVexLvJDA==} + '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} @@ -5614,6 +5626,8 @@ snapshots: ajv-formats: 3.0.1(ajv@8.18.0) fast-uri: 3.1.0 + '@fastify/busboy@3.2.0': {} + '@fastify/compress@8.3.1': dependencies: '@fastify/accept-negotiator': 2.0.1 @@ -5635,6 +5649,8 @@ snapshots: fastify-plugin: 5.1.0 toad-cache: 3.7.0 + '@fastify/deepmerge@3.2.1': {} + '@fastify/error@4.2.0': {} '@fastify/fast-json-stringify-compiler@5.0.3': @@ -5652,6 +5668,14 @@ snapshots: dependencies: dequal: 2.0.3 + '@fastify/multipart@10.0.0': + dependencies: + '@fastify/busboy': 3.2.0 + '@fastify/deepmerge': 3.2.1 + '@fastify/error': 4.2.0 + fastify-plugin: 5.1.0 + secure-json-parse: 4.1.0 + '@fastify/proxy-addr@5.1.0': dependencies: '@fastify/forwarded': 3.0.1 diff --git a/src/modules/app/app.module.ts b/src/modules/app/app.module.ts index 3ddc308..0e01a2c 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -47,6 +47,7 @@ import { MigrationService } from 'src/shared/migration'; connection: { bucket: cfg.getOrThrow('S3_BUCKET_NAME'), endpoint: cfg.getOrThrow('S3_ENDPOINT'), + region: cfg.getOrThrow('S3_REGION'), credentials: { accessKeyId: cfg.getOrThrow('S3_ACCESS_KEY'), secretAccessKey: cfg.getOrThrow('S3_SECRET_KEY'), diff --git a/src/modules/user/controller/user.controller.ts b/src/modules/user/controller/user.controller.ts index 7ac23cf..122e3f6 100644 --- a/src/modules/user/controller/user.controller.ts +++ b/src/modules/user/controller/user.controller.ts @@ -1,4 +1,4 @@ -import { Body, Get, Patch, Post, Query, UseGuards } from '@nestjs/common'; +import { BadRequestException, Body, Get, Patch, Post, Query, Req, UseGuards } from '@nestjs/common'; import { UserService } from '../user.service'; import { GetMeActivitySwagger, @@ -11,6 +11,7 @@ import { UpdateNotificationsDto, UpdateProfileDto } from '../dtos'; import { ApiBaseController, GetUserId } from '../../../shared/decorators'; import { BearerAuthGuard } from 'src/shared/guards'; import { PaginationDto } from '../../../shared/dtos'; +import { FastifyRequest } from 'fastify'; @ApiBaseController('users', 'Users') @UseGuards(BearerAuthGuard) @@ -43,10 +44,27 @@ export class UserController { @Post('me/avatar') @PostMeAvatarSwagger() - async uploadAvatar() { - return { - avatarUrl: 'https://api.dicebear.com/9.x/notionists/svg?seed=Aneka', - success: true, - }; + async uploadAvatar(@Req() req: FastifyRequest, @GetUserId() userId: string) { + if (!req.isMultipart()) { + throw new BadRequestException('Request is not multipart'); + } + + const file = await req.file(); + if (!file || file.fieldname !== 'file') { + throw new BadRequestException('Поле file не найдено'); + } + + const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/jpg']; + if (!allowedMimeTypes.includes(file.mimetype)) { + throw new BadRequestException('Недопустимый формат файла'); + } + + const buffer = await file.toBuffer(); + + return this.facade.uploadAvatar(userId, { + buffer, + filename: file.filename, + mimetype: file.mimetype, + }); } } diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 78934f1..4bbb06c 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -9,6 +9,7 @@ import { IUserRepository } from './repository/user.repository.interface'; import { UpdateNotificationsDto, UpdateProfileDto } from './dtos'; import { createId } from '@paralleldrive/cuid2'; import { S3Service } from '@libs/s3'; +import { FileUploadDto } from '@libs/s3/dtos/upload-avatar.dto'; @Injectable() export class UserService { @@ -132,25 +133,34 @@ export class UserService { }; }; - public uploadAvatar = async (id: string, avatarUrl: string) => { + public uploadAvatar = async (userId: string, fileDto: FileUploadDto) => { + const avatarUrl = await this.s3.uploadPublicFile( + fileDto.buffer, + fileDto.filename, + fileDto.mimetype, + ); + try { new URL(avatarUrl); } catch { throw new BadRequestException({ code: 'INVALID_AVATAR_URL', - message: 'Предоставлен некорректный URL аватара', + message: 'Провайдер хранилища вернул некорректный URL', }); } - await this.userRepo.updateAvatar(id, avatarUrl); + await this.userRepo.updateAvatar(userId, avatarUrl); await this.userRepo.logActivity({ id: createId(), - userId: id, + userId, eventType: 'AVATAR_CHANGED', metadata: { url: avatarUrl }, }); - return { success: true, message: '' }; + return { + success: true, + avatarUrl, + }; }; } From 9cab81ee83ae24659408a26f952289d9d11ed68a Mon Sep 17 00:00:00 2001 From: Maxim Date: Mon, 13 Apr 2026 03:34:01 +0300 Subject: [PATCH 47/47] chore(auth): add password reset and sign-up confirmation Swagger documentation --- .github/workflows/ci.yml | 4 +- .../auth/controller/auth.controller.ts | 8 ++ src/modules/auth/controller/auth.swagger.ts | 97 ++++++++++++++++++- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa9a576..4569ae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: pull_request: - branches: [dev, main] + branches: [dev, main, "feat/**"] push: - branches: [dev, main] + branches: [dev, main, "feat/**"] jobs: quality-check: diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts index 66021be..acb1689 100644 --- a/src/modules/auth/controller/auth.controller.ts +++ b/src/modules/auth/controller/auth.controller.ts @@ -4,8 +4,12 @@ import { AuthService } from '../services/auth.service'; import { PostLoginSwagger, PostLogoutSwagger, + PostPasswordResetConfirmSwagger, + PostPasswordResetSwagger, + PostPasswordResetVerifySwagger, PostRefreshSwagger, PostRegisterSwagger, + PostSignUpConfirmSwagger, } from './auth.swagger'; import { PasswordResetConfirmDto, @@ -31,6 +35,7 @@ export class AuthController { } @Post('sign-up/confirm') + @PostSignUpConfirmSwagger() @HttpCode(201) async verify( @Res({ passthrough: true }) res: FastifyReply, @@ -102,16 +107,19 @@ export class AuthController { } @Post('password/reset') + @PostPasswordResetSwagger() async resetPasswordRequest(@Body() dto: ResetPasswordDto) { return this.facade.resetPass(dto); } @Post('password/reset/verify') + @PostPasswordResetVerifySwagger() async verifyResetCode(@Body() dto: VerifyResetCodeDto) { return this.facade.verifyResetPassword(dto); } @Post('password/reset/confirm') + @PostPasswordResetConfirmSwagger() async confirmPasswordReset(@Body() dto: PasswordResetConfirmDto) { return this.facade.confirmResetPass(dto); } diff --git a/src/modules/auth/controller/auth.swagger.ts b/src/modules/auth/controller/auth.swagger.ts index 77beaef..49f4d72 100644 --- a/src/modules/auth/controller/auth.swagger.ts +++ b/src/modules/auth/controller/auth.swagger.ts @@ -3,12 +3,23 @@ import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ApiBadRequest, ApiConflict, + ApiErrorResponse, ApiForbidden, ApiNotFound, ApiUnauthorized, ApiValidationError, } from 'src/shared/error'; -import { ChangePasswordDto, Confirm2FaDto, Disable2FaDto, SignInDto, SignUpDto } from '../dtos'; +import { + ChangePasswordDto, + Confirm2FaDto, + Disable2FaDto, + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from '../dtos'; import { ActionResponse } from 'src/shared/dtos'; export const PostRegisterSwagger = () => @@ -81,6 +92,90 @@ export const PostLogoutSwagger = () => ApiUnauthorized(), ); +export const PostSignUpConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Подтверждение регистрации по коду', + description: + 'Проверяет OTP из письма, создаёт аккаунт, выдаёт access-токен в теле ответа и устанавливает refresh в httpOnly cookie.', + }), + ApiBody({ type: VerifyDto.Output }), + ApiResponse({ + status: 201, + description: 'Аккаунт подтверждён, сессия создана.', + schema: { + example: { + success: true, + message: 'Аккаунт успешно подтвержден', + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', + }, + }, + }), + ApiValidationError('Ошибка валидации (неверный формат email или длина кода)'), + ApiBadRequest('Срок регистрации истёк или сессия не найдена'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const PostPasswordResetSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Запрос кода восстановления пароля', + description: 'Отправляет одноразовый код на email, если пользователь существует.', + }), + ApiBody({ type: ResetPasswordDto.Output }), + ApiResponse({ + status: 201, + description: 'Код отправлен на почту (при успешной обработке запроса).', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат email'), + ApiErrorResponse( + 422, + 'INVALID_EMAIL_FORMAT', + 'Указанный email адрес имеет некорректный формат', + ), + ApiNotFound('Пользователь с таким email не найден'), + ); + +export const PostPasswordResetVerifySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Проверка кода восстановления пароля', + description: 'Проверяет код из письма и помечает сессию сброса как подтверждённую.', + }), + ApiBody({ type: VerifyResetCodeDto.Output }), + ApiResponse({ + status: 201, + description: 'Код подтверждён, можно задать новый пароль.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (email или формат кода)'), + ApiBadRequest('Время подтверждения истекло или запрос не найден'), + ApiBadRequest('Неверный или истёкший код подтверждения'), + ); + +export const PostPasswordResetConfirmSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Установка нового пароля после сброса', + description: 'Доступно только после успешной проверки кода на шаге verify.', + }), + ApiBody({ type: PasswordResetConfirmDto.Output }), + ApiResponse({ + status: 201, + description: 'Пароль успешно изменён.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (пароли не совпадают или неверная длина)'), + ApiBadRequest('Сессия восстановления не найдена или истекла'), + ApiForbidden(), + ApiErrorResponse( + 500, + 'PASSWORD_UPDATE_FAILED', + 'Не удалось обновить пароль. Попробуйте позже.', + ), + ); + export const GetSessionsSwagger = () => applyDecorators( ApiOperation({