diff --git a/.env.example b/.env.example index d3f2c1a..7421df7 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 @@ -18,6 +18,30 @@ DB_SCHEMA=base DATABASE_URL=postgres://${DB_USERNAME}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_DATABASE} # --- REDIS --- -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_EXTERNAL_PORT=6380 +# in the docker network will be, not show port redis, at prod env +# REDIS_HOST=redis +# at development mode +REDIS_HOST=127.0.0.1 +REDIS_PORT=7000 + +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 + +# --- 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 + +S3_BUCKET_NAME='' +S3_ENDPOINT='' +S3_REGION='' +S3_ACCESS_KEY='' +S3_SECRET_KEY='' \ No newline at end of file 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3034e40..4569ae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,37 @@ name: CI on: - pull_request: - branches: [dev, main] - push: - branches: [dev, main] + pull_request: + branches: [dev, main, "feat/**"] + push: + branches: [dev, main, "feat/**"] jobs: - quality-check: - name: Lint & Test - runs-on: ubuntu-latest + quality-check: + name: Lint & Test + runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 + steps: + - name: Checkout repository + uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@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: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Install dependencies + run: pnpm install --frozen-lockfile - - name: Run Lint - run: pnpm run lint + - name: Run Lint + run: pnpm run lint - - name: Type Check - run: pnpm exec tsc --noEmit + - name: Type Check + run: pnpm exec tsc --noEmit - - name: Run Tests - run: pnpm run test + - name: Run Tests + run: pnpm run test 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 new file mode 100644 index 0000000..57e9bf6 --- /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/dev/compose.dev.yaml --env-file .env --profile infra up --build -d -V +``` diff --git a/infra/compose.dev.yaml b/infra/compose.dev.yaml deleted file mode 100644 index e7c4d67..0000000 --- a/infra/compose.dev.yaml +++ /dev/null @@ -1,54 +0,0 @@ -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: 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/dev/compose.dev.yaml b/infra/dev/compose.dev.yaml new file mode 100644 index 0000000..19e2855 --- /dev/null +++ b/infra/dev/compose.dev.yaml @@ -0,0 +1,121 @@ +version: "3.9" + +name: task-tracker-api + +services: + api: + hostname: api + container_name: api + image: ghcr.io/task-tracker-lab/task-tracker-backend:feat-user + env_file: + - .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 + container_name: database + image: postgres:16-alpine + restart: always + env_file: + - .env + environment: + POSTGRES_USER: ${DB_USERNAME} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_DATABASE} + ports: + - "6000:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: + [ + "CMD-SHELL", + 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -q || exit 1', + ] + interval: 5s + timeout: 5s + 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 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + 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: + name: task-tracker-gateway 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/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'], 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/libs/config/src/config.schema.ts b/libs/config/src/config.schema.ts index 348a00f..e28d54f 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), @@ -9,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() @@ -29,6 +34,57 @@ 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'), + 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/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/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/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/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..47d8a8d --- /dev/null +++ b/libs/s3/src/s3.service.ts @@ -0,0 +1,50 @@ +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) + private readonly options: S3ModuleOptions, + ) { + const { bucket, credentials, endpoint, region } = options.connection; + this.bucket = bucket; + this.endpoint = endpoint as string; + + this.s3Client = new S3Client({ + region, + endpoint, + credentials, + ...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/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/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/nest-cli.json b/nest-cli.json index 03f8cdf..572e181 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -42,6 +42,15 @@ "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" + } } } } diff --git a/package.json b/package.json index f202d2e..f9cd3cf 100644 --- a/package.json +++ b/package.json @@ -24,24 +24,46 @@ "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", "@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", "@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", + "@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", "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", + "passport-local": "^1.0.0", "pg": "^8.20.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "ua-parser-js": "^2.0.9", "zod": "^4.3.6" }, "devDependencies": { @@ -51,8 +73,12 @@ "@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", "@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 91a0420..4d0faec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,21 @@ 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) + '@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 @@ -17,9 +32,18 @@ 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 + '@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) @@ -29,30 +53,69 @@ 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)) + 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) '@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) + 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) 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 + 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 + 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 @@ -62,6 +125,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 @@ -84,12 +150,24 @@ 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 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 '@types/pg': specifier: ^8.20.0 version: 8.20.0 '@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) @@ -178,6 +256,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'} @@ -206,6 +451,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'} @@ -307,6 +573,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' @@ -783,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==} @@ -792,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==} @@ -807,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==} @@ -816,6 +1094,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'} @@ -972,6 +1253,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==} @@ -1005,12 +1289,62 @@ 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: '@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/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'} @@ -1061,6 +1395,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: @@ -1074,6 +1413,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: @@ -1109,6 +1454,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: @@ -1133,6 +1526,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'} @@ -1154,12 +1551,38 @@ 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==} '@paralleldrive/cuid2@2.3.1': resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + '@paralleldrive/cuid2@3.3.0': + 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==} @@ -1277,6 +1700,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'} @@ -1285,105 +1711,317 @@ packages: resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@smithy/chunked-blob-reader-native@4.2.3': + resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + engines: {node: '>=18.0.0'} - '@swc/core-darwin-arm64@1.15.24': - resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==} - engines: {node: '>=10'} - cpu: [arm64] - os: [darwin] + '@smithy/chunked-blob-reader@5.2.2': + resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} + engines: {node: '>=18.0.0'} - '@swc/core-darwin-x64@1.15.24': - resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==} - engines: {node: '>=10'} - cpu: [x64] - os: [darwin] + '@smithy/config-resolver@4.4.14': + resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-arm-gnueabihf@1.15.24': - resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux] + '@smithy/core@3.23.14': + resolution: {integrity: sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-arm64-gnu@1.15.24': - resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [glibc] + '@smithy/credential-provider-imds@4.2.13': + resolution: {integrity: sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-arm64-musl@1.15.24': - resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux] - libc: [musl] + '@smithy/eventstream-codec@4.2.13': + resolution: {integrity: sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-ppc64-gnu@1.15.24': - resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==} - engines: {node: '>=10'} - cpu: [ppc64] - os: [linux] - libc: [glibc] + '@smithy/eventstream-serde-browser@4.2.13': + resolution: {integrity: sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-s390x-gnu@1.15.24': - resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==} - engines: {node: '>=10'} - cpu: [s390x] - os: [linux] - libc: [glibc] + '@smithy/eventstream-serde-config-resolver@4.3.13': + resolution: {integrity: sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-x64-gnu@1.15.24': - resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [glibc] + '@smithy/eventstream-serde-node@4.2.13': + resolution: {integrity: sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==} + engines: {node: '>=18.0.0'} - '@swc/core-linux-x64-musl@1.15.24': - resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux] - libc: [musl] + '@smithy/eventstream-serde-universal@4.2.13': + resolution: {integrity: sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==} + engines: {node: '>=18.0.0'} - '@swc/core-win32-arm64-msvc@1.15.24': - resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] + '@smithy/fetch-http-handler@5.3.16': + resolution: {integrity: sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==} + engines: {node: '>=18.0.0'} - '@swc/core-win32-ia32-msvc@1.15.24': - resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==} - engines: {node: '>=10'} - cpu: [ia32] - os: [win32] + '@smithy/hash-blob-browser@4.2.14': + resolution: {integrity: sha512-rtQ5es8r/5v4rav7q5QTsfx9CtCyzrz/g7ZZZBH2xtMmd6G/KQrLOWfSHTvFOUPlVy59RQvxeBYJaLRoybMEyA==} + engines: {node: '>=18.0.0'} - '@swc/core-win32-x64-msvc@1.15.24': - resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] + '@smithy/hash-node@4.2.13': + resolution: {integrity: sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==} + engines: {node: '>=18.0.0'} - '@swc/core@1.15.24': - resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==} - engines: {node: '>=10'} - peerDependencies: - '@swc/helpers': '>=0.5.17' - peerDependenciesMeta: - '@swc/helpers': - optional: true + '@smithy/hash-stream-node@4.2.13': + resolution: {integrity: sha512-WdQ7HwUjINXETeh6dqUeob1UHIYx8kAn9PSp1HhM2WWegiZBYVy2WXIs1lB07SZLan/udys9SBnQGt9MQbDpdg==} + engines: {node: '>=18.0.0'} - '@swc/counter@0.1.3': - resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@smithy/invalid-dependency@4.2.13': + resolution: {integrity: sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==} + engines: {node: '>=18.0.0'} - '@swc/types@0.1.26': - resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} - '@tokenizer/inflate@0.4.1': - resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} - engines: {node: '>=18'} + '@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==} + + '@swc/core-darwin-arm64@1.15.24': + resolution: {integrity: sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.24': + resolution: {integrity: sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.24': + resolution: {integrity: sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.24': + resolution: {integrity: sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.24': + resolution: {integrity: sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.24': + resolution: {integrity: sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.24': + resolution: {integrity: sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.24': + resolution: {integrity: sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.24': + resolution: {integrity: sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.24': + resolution: {integrity: sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.24': + resolution: {integrity: sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.24': + resolution: {integrity: sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.24': + resolution: {integrity: sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} @@ -1403,9 +2041,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==} @@ -1421,27 +2065,72 @@ 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/nodemailer@8.0.0': + resolution: {integrity: sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==} + + '@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==} '@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} @@ -1657,6 +2346,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'} @@ -1688,6 +2380,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==} @@ -1711,6 +2407,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==} @@ -1736,12 +2435,22 @@ 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==} 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'} + brace-expansion@1.1.13: resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} @@ -1761,6 +2470,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==} @@ -1770,6 +2482,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'} @@ -1782,6 +2497,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==} @@ -1796,6 +2515,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'} @@ -1804,6 +2527,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'} @@ -1836,6 +2563,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'} @@ -1937,6 +2668,15 @@ 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'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1964,6 +2704,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'} @@ -1972,6 +2716,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'} @@ -2119,9 +2866,21 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + 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==} + 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==} @@ -2143,6 +2902,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==} @@ -2310,6 +3072,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==} @@ -2336,6 +3105,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'} @@ -2452,6 +3224,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'} @@ -2513,6 +3290,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'} @@ -2556,6 +3337,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'} @@ -2582,6 +3366,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'} @@ -2629,6 +3418,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==} @@ -2744,6 +3543,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==} @@ -2753,6 +3576,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==} @@ -2777,6 +3603,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==} @@ -2853,6 +3683,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'} @@ -2867,6 +3701,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} @@ -2896,13 +3737,29 @@ 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-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 + node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} - object-inspect@1.13.4: + 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'} @@ -2932,6 +3789,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'} @@ -2948,10 +3808,29 @@ 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'} + 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'} @@ -2974,6 +3853,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==} @@ -3125,6 +4007,17 @@ 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-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3299,6 +4192,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'} @@ -3347,6 +4243,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'} @@ -3505,6 +4404,18 @@ 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 + + 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'} @@ -3541,6 +4452,14 @@ 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'} + + 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==} @@ -3666,10 +4585,17 @@ 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'} + 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'} @@ -3777,6 +4703,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 @@ -3800,6 +5188,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 @@ -3947,6 +5361,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 @@ -4210,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 @@ -4231,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': @@ -4248,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 @@ -4270,6 +5698,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 @@ -4422,6 +5855,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 @@ -4457,6 +5892,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 @@ -4464,6 +5917,44 @@ 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/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) @@ -4524,12 +6015,23 @@ 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/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/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)(@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 @@ -4545,6 +6047,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: @@ -4571,6 +6074,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) @@ -4585,6 +6098,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 @@ -4603,80 +6118,450 @@ 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': dependencies: - '@noble/hashes': 1.8.0 + '@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 + + '@phc/format@1.0.0': {} + + '@pinojs/redact@0.4.0': {} + + '@pkgr/core@0.2.9': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + + '@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 + + '@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 - '@pinojs/redact@0.4.0': {} + '@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 - '@pkgr/core@0.2.9': {} + '@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 - '@rolldown/binding-android-arm64@1.0.0-rc.15': - optional: true + '@smithy/types@4.14.0': + dependencies: + tslib: 2.8.1 - '@rolldown/binding-darwin-arm64@1.0.0-rc.15': - optional: true + '@smithy/url-parser@4.2.13': + dependencies: + '@smithy/querystring-parser': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@rolldown/binding-darwin-x64@1.0.0-rc.15': - optional: true + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 - '@rolldown/binding-freebsd-x64@1.0.0-rc.15': - optional: true + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': - optional: true + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': - optional: true + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': - optional: true + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': - optional: true + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': - optional: true + '@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 - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': - optional: true + '@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 - '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': - optional: true + '@smithy/util-endpoints@3.3.4': + dependencies: + '@smithy/node-config-provider': 4.3.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': - optional: true + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 - '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + '@smithy/util-middleware@4.2.13': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - optional: true + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': - optional: true + '@smithy/util-retry@4.3.1': + dependencies: + '@smithy/service-error-classification': 4.2.13 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': - optional: true + '@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 - '@rolldown/pluginutils@1.0.0-rc.15': {} + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 - '@rollup/pluginutils@5.3.0': + '@smithy/util-utf8@2.3.0': dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.4 + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 - '@scarf/scarf@1.4.0': {} + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 - '@simple-libs/child-process-utils@1.0.2': + '@smithy/util-waiter@4.2.15': dependencies: - '@simple-libs/stream-utils': 1.2.0 + '@smithy/types': 4.14.0 + tslib: 2.8.1 - '@simple-libs/stream-utils@1.2.0': {} + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 '@standard-schema/spec@1.1.0': {} @@ -4762,11 +6647,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': {} @@ -4783,22 +6677,81 @@ 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/nodemailer@8.0.0': + dependencies: + '@types/node': 20.19.39 + + '@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 @@ -4811,6 +6764,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 @@ -5090,6 +7045,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: @@ -5110,6 +7070,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: {} @@ -5128,6 +7095,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: {} @@ -5145,6 +7114,8 @@ snapshots: baseline-browser-mapping@2.10.17: {} + bignumber.js@9.3.1: {} + bintrees@1.0.2: {} bl@4.1.0: @@ -5153,6 +7124,20 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bowser@2.14.1: {} + + 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 @@ -5178,6 +7163,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: @@ -5190,6 +7177,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 @@ -5202,6 +7201,9 @@ snapshots: callsites@3.1.0: {} + camelcase@6.3.0: + optional: true + caniuse-lite@1.0.30001787: {} chai@6.2.2: {} @@ -5213,12 +7215,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 @@ -5250,6 +7258,8 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5334,6 +7344,15 @@ 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 + cross-spawn: 7.0.6 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5354,10 +7373,14 @@ snapshots: delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} dequal@2.0.3: {} + detect-europe-js@0.1.2: {} + detect-libc@2.1.2: {} dezalgo@1.0.4: @@ -5425,8 +7448,18 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 + ecdsa-sig-formatter@1.0.11: + 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: {} + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -5444,6 +7477,8 @@ snapshots: environment@1.1.0: {} + error-causes@3.0.2: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -5694,6 +7729,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: @@ -5735,6 +7780,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 @@ -5887,6 +7936,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: {} @@ -5937,6 +7995,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: {} @@ -5963,6 +8035,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-standalone-pwa@0.1.1: {} + is-unicode-supported@0.1.0: {} isarray@1.0.0: {} @@ -5984,6 +8058,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 @@ -6024,6 +8104,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 @@ -6120,12 +8224,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: {} @@ -6149,6 +8271,8 @@ snapshots: lru-cache@11.3.3: {} + luxon@3.7.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6210,6 +8334,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 @@ -6220,6 +8348,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: {} @@ -6239,12 +8383,23 @@ 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-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-gyp-build@4.8.4: {} + node-releases@2.0.37: {} + nodemailer@8.0.5: {} + object-inspect@1.13.4: {} obug@2.1.1: {} @@ -6284,6 +8439,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 @@ -6303,8 +8467,27 @@ 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-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -6320,6 +8503,8 @@ snapshots: pathe@2.0.3: {} + pause@0.0.1: {} + peek-stream@1.1.3: dependencies: buffer-from: 1.1.2 @@ -6475,6 +8660,16 @@ snapshots: real-require@0.2.0: {} + 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 + reflect-metadata@0.2.2: {} require-directory@2.1.1: {} @@ -6646,6 +8841,8 @@ snapshots: stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + statuses@2.0.2: {} std-env@4.0.0: {} @@ -6691,6 +8888,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.2.3: {} + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 @@ -6859,6 +9058,17 @@ 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 + + uglify-js@3.19.3: + optional: true + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 @@ -6897,6 +9107,10 @@ snapshots: util-deprecate@1.0.2: {} + 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): @@ -7000,8 +9214,15 @@ 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: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 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 c55df26..0e01a2c 100644 --- a/src/modules/app/app.module.ts +++ b/src/modules/app/app.module.ts @@ -1,13 +1,22 @@ 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'; 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'; +import { AuthModule } from '../auth'; +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'; +import { S3Module } from '@libs/s3'; +import { MigrationService } from 'src/shared/migration'; @Module({ imports: [ @@ -31,17 +40,54 @@ import { HealthModule } from '@libs/health'; }; }, }), + S3Module.registerAsync({ + inject: [ConfigService], + global: true, + useFactory: (cfg: ConfigService) => ({ + 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'), + }, + }, + // FOR MINIO COMPARTABLE + config: { forcePathStyle: true }, + }), + }), + BullModule.forRootAsync({ + inject: [ConfigService], + useFactory: (cfg: ConfigService) => ({ + connection: { + host: cfg.getOrThrow('REDIS_HOST'), + port: cfg.get('REDIS_PORT'), + }, + }), + }), + AuthModule, + UserModule, + BullBoardModule.forRoot({ + route: '/queues', + adapter: FastifyAdapter, + }), HealthModule.register('gateway'), ], - controllers: [AppController], providers: [ + MigrationService, + { + provide: 'IMailPort', + useClass: MailAdapter, + }, + MailProcessor, { provide: APP_PIPE, useClass: ZodValidationPipe, }, { provide: APP_FILTER, - useClass: ZodValidationException, + useClass: GlobalExceptionFilter, }, ], }) diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..1b8555d --- /dev/null +++ b/src/modules/auth/auth.module.ts @@ -0,0 +1,75 @@ +import { Module, forwardRef } from '@nestjs/common'; +import { UserModule } from '../user'; +import { AuthController } from './controller'; +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'; +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: [ + 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, + }, + }), + }), + RedisModule.forRootAsync({ + inject: [ConfigService], + useFactory: async (cfg: ConfigService) => { + const host = cfg.getOrThrow('REDIS_HOST', { infer: true }); + const port = cfg.get('REDIS_PORT'); + const url = `redis://${host}${port ? `:${port}` : ''}`; + + return { + type: 'single', + url, + options: { + retryStrategy(times) { + return Math.min(times * 50, 2000); + }, + commandTimeout: 3000, + }, + }; + }, + }), + BullModule.registerQueue({ + name: Queues.MAIL, + }), + BullBoardModule.forFeature({ + name: Queues.MAIL, + adapter: BullMQAdapter, + }), + forwardRef(() => UserModule), + ], + controllers: [AuthController], + providers: [ + AuthService, + TokenService, + CookieStrategy, + BearerStrategy, + { provide: 'ISessionRepository', useClass: SessionRepository }, + ], + exports: [], +}) +export class AuthModule {} diff --git a/src/modules/auth/controller/auth.controller.ts b/src/modules/auth/controller/auth.controller.ts new file mode 100644 index 0000000..acb1689 --- /dev/null +++ b/src/modules/auth/controller/auth.controller.ts @@ -0,0 +1,126 @@ +import { ApiBaseController } from '../../../shared/decorators'; +import { Body, HttpCode, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { AuthService } from '../services/auth.service'; +import { + PostLoginSwagger, + PostLogoutSwagger, + PostPasswordResetConfirmSwagger, + PostPasswordResetSwagger, + PostPasswordResetVerifySwagger, + PostRefreshSwagger, + PostRegisterSwagger, + PostSignUpConfirmSwagger, +} from './auth.swagger'; +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'; + +@ApiBaseController('auth', 'Auth') +export class AuthController { + constructor(private readonly facade: AuthService) {} + + @Post('sign-up') + @PostRegisterSwagger() + @HttpCode(202) + async signUp(@Body() dto: SignUpDto) { + return this.facade.signUp(dto); + } + + @Post('sign-up/confirm') + @PostSignUpConfirmSwagger() + @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() + async signIn( + @Res({ passthrough: true }) res: FastifyReply, + @Req() req: FastifyRequest, + @Body() dto: SignInDto, + ) { + const meta = getDeviceMeta(req); + const { tokens, ...response } = await this.facade.signIn(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() + 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(@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 }; + } + + @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 new file mode 100644 index 0000000..49f4d72 --- /dev/null +++ b/src/modules/auth/controller/auth.swagger.ts @@ -0,0 +1,271 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ApiBadRequest, + ApiConflict, + ApiErrorResponse, + ApiForbidden, + ApiNotFound, + ApiUnauthorized, + ApiValidationError, +} from 'src/shared/error'; +import { + ChangePasswordDto, + Confirm2FaDto, + Disable2FaDto, + PasswordResetConfirmDto, + ResetPasswordDto, + SignInDto, + SignUpDto, + VerifyDto, + VerifyResetCodeDto, +} from '../dtos'; +import { ActionResponse } from 'src/shared/dtos'; + +export const PostRegisterSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Регистрация нового пользователя', + description: 'Создает пользователя, базовые настройки безопасности и уведомлений.', + }), + ApiBody({ type: SignUpDto.Output }), + ApiResponse({ + status: 201, + description: 'Пользователь успешно зарегистрирован.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации данных (например, неверный формат email)'), + ApiConflict('Пользователь с таким email уже существует'), + ); + +export const PostLoginSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Вход в систему', + description: + 'Возвращает Access/Refresh токены. Если у пользователя включена 2FA, вернет временный токен.', + }), + ApiBody({ type: SignInDto.Output }), + ApiResponse({ + status: 200, + description: 'Успешный вход.', + schema: { + example: { + success: true, + message: false, + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', + }, + }, + }), + ApiBadRequest('Неверный формат email'), + ApiUnauthorized('Неверный email или пароль'), + ); + +export const PostRefreshSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновление токенов', + description: 'Выдает новую пару Access и Refresh токенов.', + }), + ApiResponse({ + status: 200, + description: 'Токены успешно обновлены.', + schema: { + example: { + success: true, + token: 'eyJhbGciOiJIUzI1NiIsInR5c...', + message: 'def50200508a1768c7e...', + }, + }, + }), + ApiBadRequest('Ошибка валидации (не передан refresh токен)'), + ApiUnauthorized('Refresh токен недействителен, истек или отозван'), + ); + +export const PostLogoutSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Выход из системы', + description: 'Удаляет текущую сессию пользователя из Redis.', + }), + ApiResponse({ status: 200, description: 'Успешный выход.', type: ActionResponse.Output }), + 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({ + 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 DeleteSessionSwagger = () => + 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.Output }), + 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.Output }), + ApiResponse({ status: 200, description: 'Двухфакторная аутентификация успешно включена.' }), + ApiBadRequest('Неверный код подтверждения'), + ApiUnauthorized(), + ); + +export const PostConfirm2faSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Отключение 2FA', + description: + 'Отключает двухфакторную аутентификацию (требует подтверждения паролем или текущим кодом).', + }), + ApiBody({ type: Disable2FaDto.Output }), + 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/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..5aeac05 --- /dev/null +++ b/src/modules/auth/dtos/auth.dto.ts @@ -0,0 +1,58 @@ +import { createZodDto } from 'nestjs-zod'; +import { z } from 'zod/v4'; + +export const SignInSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z.string().describe('Пароль пользователя'), + }) + .describe('Схема входа в систему'); + +export class SignInDto extends createZodDto(SignInSchema) {} + +export const SignUpSchema = z + .object({ + email: z.email('Некорректный формат email').describe('Email пользователя'), + password: z + .string() + .min(8, 'Пароль должен содержать минимум 8 символов') + .max(32, 'Пароль должен содержать максимум 32 символа') + .describe('Пароль (минимум 8 символов)'), + firstName: z + .string() + .min(2, 'Имя должно содержать минимум 2 символа') + .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 SignUpDto extends createZodDto(SignUpSchema) {} + +export const VerifySchema = z + .object({ + email: z + .string() + .email('Некорректный формат email') + .describe('Email пользователя, на который был отправлен код'), + code: z + .string() + .length(6, 'Код должен содержать ровно 6 символов') + .describe('6-значный OTP код подтверждения'), + }) + .describe('Схема верификации OTP кода'); + +export class VerifyDto extends createZodDto(VerifySchema) {} 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..e0a260f --- /dev/null +++ b/src/modules/auth/dtos/password.dto.ts @@ -0,0 +1,45 @@ +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) {} + +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/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/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/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/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..093b1f3 --- /dev/null +++ b/src/modules/auth/services/auth.service.ts @@ -0,0 +1,389 @@ +import { + BadRequestException, + ConflictException, + ForbiddenException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, + UnauthorizedException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { InjectRedis } from '@nestjs-modules/ioredis'; +import Redis from 'ioredis'; +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, 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 { + constructor( + @InjectRedis() + 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) => { + 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({ email: 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 }, + }; + + await this.redis.set(`reg:${dto.email}`, JSON.stringify(data), 'EX', 900); + + 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, + 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 verifyResult = await verifyOTP({ + token: dto.code, + secret: userData.otp.secret, + algorithm: 'sha256', + digits: 6, + period: 900, + strategy: 'totp', + }); + + if (!verifyResult.valid) { + 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(Date.now() + 3 * 24 * 60 * 60 * 1000), + ...meta, + }); + const { access, refresh } = await this.tokenService.generateTokens(user, session.id); + + await this.redis.del(redisKey); + + return { + success: true, + tokens: { access, refresh }, + message: 'Аккаунт успешно подтвержден', + }; + }; + + public signIn = async (dto: SignInDto, meta: DeviceMetadata) => { + const { user, security } = await this.findUserCommand.execute({ email: dto.email }); + + if (!user || !security) { + throw new UnauthorizedException({ + code: 'INVALID_CREDENTIALS', + message: 'Неверный email или пароль', + }); + } + + const isPasswordValid = await argon.verify(security.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(Date.now() + 3 * 24 * 60 * 60 * 1000), + ...meta, + }); + + const { access, refresh } = await this.tokenService.generateTokens(user, id); + + return { + success: true, + tokens: { + access, + refresh, + }, + message: 'Вы успешно вошли в систему', + }; + }; + + 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: 'Ваша сессия была отозвана или завершена', + }); + } + + 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: 'Успешно вышли из системы!' }; + }; + + 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 verifyResult = await verifyOTP({ + token: dto.code, + secret: resetSession.otp.secret, + digits: 6, + period: 900, + strategy: 'totp', + }); + + if (!verifyResult.valid) { + 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); + const isUpdated = await this.updateUserPass.execute(dto.email, hashed); + + if (!isUpdated) { + throw new InternalServerErrorException({ + code: 'PASSWORD_UPDATE_FAILED', + message: 'Не удалось обновить пароль. Попробуйте позже.', + }); + } + await this.redis.del(redisKey); + + return { + success: true, + message: 'Пароль успешно изменен. Теперь вы можете войти в аккаунт.', + }; + }; +} 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/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/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..1e44d15 --- /dev/null +++ b/src/modules/user/commands/find-one.command.ts @@ -0,0 +1,27 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { IUserRepository } from '../repository/user.repository.interface'; +import type { UserWithSecurity } from '../entities/user.domain'; + +@Injectable() +export class FindOneUserCommand { + constructor( + @Inject('IUserRepository') + private readonly repository: IUserRepository, + ) {} + + 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/commands/index.ts b/src/modules/user/commands/index.ts new file mode 100644 index 0000000..7a59139 --- /dev/null +++ b/src/modules/user/commands/index.ts @@ -0,0 +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..3ad7228 --- /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 }, + }); + } + + return this.repository.updatePasswordHash(user.id, password); + } +} 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..122e3f6 --- /dev/null +++ b/src/modules/user/controller/user.controller.ts @@ -0,0 +1,70 @@ +import { BadRequestException, Body, Get, Patch, Post, Query, Req, UseGuards } from '@nestjs/common'; +import { UserService } from '../user.service'; +import { + GetMeActivitySwagger, + GetMeSwagger, + PatchMeNotificationsSwagger, + PatchMeSwagger, + PostMeAvatarSwagger, +} from './user.swagger'; +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) +export class UserController { + constructor(private readonly facade: UserService) {} + + @Get('me') + @GetMeSwagger() + async getProfile(@GetUserId() id: string) { + return this.facade.getProfile(id); + } + + @Patch('me') + @PatchMeSwagger() + async updateProfile(@Body() dto: UpdateProfileDto, @GetUserId() id: string) { + return this.facade.updateProfile(id, dto); + } + + @Patch('me/notifications') + @PatchMeNotificationsSwagger() + async updateNotifications(@Body() settings: UpdateNotificationsDto, @GetUserId() id: string) { + return this.facade.updateNotifications(id, settings); + } + + @Get('me/activity') + @GetMeActivitySwagger() + async getActivity(@Query() query: PaginationDto, @GetUserId() id: string) { + return this.facade.getActivity(id, query.page, query.limit); + } + + @Post('me/avatar') + @PostMeAvatarSwagger() + 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/controller/user.swagger.ts b/src/modules/user/controller/user.swagger.ts new file mode 100644 index 0000000..423699c --- /dev/null +++ b/src/modules/user/controller/user.swagger.ts @@ -0,0 +1,134 @@ +import { + ApiBody, + ApiConsumes, + ApiExtraModels, + ApiOperation, + ApiQuery, + ApiResponse, +} from '@nestjs/swagger'; +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( + ApiExtraModels(UserResponse.Output), + ApiOperation({ + summary: 'Получить профиль текущего пользователя', + description: + 'Возвращает полную структуру профиля, включая вложенные объекты безопасности и настроек.', + }), + ApiResponse({ + status: 200, + description: 'Данные профиля успешно получены.', + type: UserResponse.Output, + }), + ApiUnauthorized(), + ); + +export const PatchMeSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить данные профиля', + description: 'Позволяет точечно обновить имя, bio, часовой пояс и язык интерфейса.', + }), + ApiBody({ type: UpdateProfileDto.Output }), + ApiResponse({ + status: 200, + description: 'Профиль успешно обновлен.', + type: ActionResponse.Output, + }), + ApiValidationError('Ошибка валидации (например, слишком короткое имя)', [ + { + field: 'fullName', + message: 'Строка должна содержать минимум 2 символа', + code: 'too_small', + }, + ]), + ApiUnauthorized(), + ); + +export const PatchMeNotificationsSwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Обновить настройки уведомлений', + description: 'Частичное обновление настроек email и push уведомлений.', + }), + ApiBody({ + type: UpdateNotificationsDto.Output, + }), + ApiResponse({ + status: 200, + description: 'Настройки успешно сохранены.', + type: ActionResponse.Output, + }), + ApiValidationError('Некорректный формат настроек'), + ApiUnauthorized(), + ); + +export const GetMeActivitySwagger = () => + applyDecorators( + ApiOperation({ + summary: 'Получить ленту активности пользователя', + description: 'Возвращает список последних действий пользователя (логи).', + }), + ApiQuery({ + name: 'limit', + required: false, + type: String, + description: 'Количество записей для вывода (по умолчанию 10)', + example: '15', + }), + ApiResponse({ + status: 200, + description: 'Список активностей успешно получен.', + schema: { + 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(), + ); + +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: 'Аватар успешно загружен.', + type: ActionResponse.Output, + }), + ApiBadRequest('Файл не передан или имеет неверный формат'), + ApiUnauthorized(), + ); 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..d342b79 --- /dev/null +++ b/src/modules/user/dtos/user.dto.ts @@ -0,0 +1,75 @@ +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('Данные безопасности аккаунта'); + +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().describe('Уникальный идентификатор (CUID/UUID)'), + email: z.string().email().describe('Электронная почта'), + profile: ProfileSchema, + security: SecuritySchema, + notifications: NotificationsSchema, +}); + +export class UserResponse extends createZodDto(UserSchema) {} + +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/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.domain.ts b/src/modules/user/entities/user.domain.ts new file mode 100644 index 0000000..0721065 --- /dev/null +++ b/src/modules/user/entities/user.domain.ts @@ -0,0 +1,25 @@ +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 = Pick; + +export type UserActivity = InferSelectModel; +export type NewUserActivity = InferInsertModel; + +export type UserProfile = { + user: User; + security: Pick; + notifications: NotificationSettings['settings']; +}; + +export type UserWithSecurity = { + user: User; + security: Pick; +}; diff --git a/src/modules/user/entities/user.entity.ts b/src/modules/user/entities/user.entity.ts new file mode 100644 index 0000000..9d06268 --- /dev/null +++ b/src/modules/user/entities/user.entity.ts @@ -0,0 +1,60 @@ +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()), + + 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 }), + 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..3b9d53d --- /dev/null +++ b/src/modules/user/index.ts @@ -0,0 +1,3 @@ +export { UserModule } from './user.module'; +export { UserRepository } from './repository/user.repository'; +export { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; 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..e2c4bbe --- /dev/null +++ b/src/modules/user/repository/user.repository.interface.ts @@ -0,0 +1,28 @@ +import type { + NewUser, + NewUserActivity, + User, + UserActivity, + UserNotifications, + UserProfile, + UserWithSecurity, +} from '../entities/user.domain'; + +export interface IUserRepository { + create(data: NewUser): Promise; + findById(id: string): Promise; + findByEmail(email: string): Promise; + findProfile(id: string): Promise; + findActivityByUser( + userId: string, + options: { limit: number; offset: number }, + ): Promise<{ + items: UserActivity[]; + total: number; + }>; + 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 new file mode 100644 index 0000000..757958b --- /dev/null +++ b/src/modules/user/repository/user.repository.ts @@ -0,0 +1,142 @@ +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 } 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 db: DatabaseService, + ) {} + + 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)); + } + + 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; + + 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: row.users, + security: { + passwordHash: row.user_security.passwordHash, + }, + }; + } + + async findByEmail(email: string) { + const [row] = await this.fullUserQuery.where(eq(sc.users.email, email.toLowerCase())); + if (!row || !row.user_security) return null; + return { + user: row.users, + security: { + passwordHash: row.user_security.passwordHash, + }, + }; + } + + async findSecurityByUserId(userId: string) { + const [result] = await this.db + .select() + .from(sc.userSecurity) + .where(eq(sc.userSecurity.userId, userId)); + return result || null; + } + + 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({ + userId: newUser.id, + }); + + return newUser; + }); + } + + 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)); + return (rowCount ?? 0) > 0; + } + + async updateNotifications(id: string, settings: UserNotifications['settings']) { + 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) { + 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) { + 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) { + 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.db + .select({ value: count() }) + .from(sc.userActivity) + .where(eq(sc.userActivity.userId, userId)), + this.db + .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.module.ts b/src/modules/user/user.module.ts new file mode 100644 index 0000000..a5b7941 --- /dev/null +++ b/src/modules/user/user.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import { UserController } from './controller'; +import { UserService } from './user.service'; +import { UserRepository } from './repository/user.repository'; +import { CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand } from './commands'; + +const REPOSITORY = { + provide: 'IUserRepository', + useClass: UserRepository, +}; + +const COMMANDS = [CreateUserCommand, FindOneUserCommand, UpdatePassUserCommand]; + +@Module({ + imports: [], + controllers: [UserController], + providers: [...COMMANDS, REPOSITORY, UserService], + exports: [...COMMANDS], +}) +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..4bbb06c --- /dev/null +++ b/src/modules/user/user.service.ts @@ -0,0 +1,166 @@ +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'; +import { S3Service } from '@libs/s3'; +import { FileUploadDto } from '@libs/s3/dtos/upload-avatar.dto'; + +@Injectable() +export class UserService { + constructor( + @Inject('IUserRepository') + private readonly userRepo: IUserRepository, + private readonly s3: S3Service, + ) {} + + private throwUserNotFound() { + throw new NotFoundException({ + code: 'USER_NOT_FOUND', + message: 'Пользователь не найден в системе', + }); + } + + public getProfile = async (userId: string) => { + const { user, notifications, security } = await this.userRepo.findProfile(userId); + if (!user) this.throwUserNotFound(); + const { id, email, ...profile } = user; + + return { + id, + email, + profile, + security, + notifications, + }; + }; + + public updateProfile = async (id: string, dto: UpdateProfileDto) => { + const keysToUpdate = Object.keys(dto); + if (keysToUpdate.length === 0) { + return { + success: true, + message: 'Изменений не обнаружено', + }; + } + + 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: 'Профиль успешно обновлен', + }; + } catch (error) { + throw error; + } + }; + + public updateNotifications = async (id: string, dto: UpdateNotificationsDto) => { + const keysToUpdate = Object.keys(dto); + if (keysToUpdate.length === 0) { + return { + success: true, + message: 'Изменений не обнаружено', + }; + } + + const user = await this.userRepo.findById(id); + if (!user) this.throwUserNotFound(); + + try { + const isUpdated = await this.userRepo.updateNotifications(id, { + email: dto.email, + push: dto.push, + }); + + if (!isUpdated) { + throw new InternalServerErrorException( + 'Ошибка при сохранении настроек уведомлений', + ); + } + + 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) => { + const safeLimit = Math.min(limit, 50); + const offset = (page - 1) * safeLimit; + + 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 (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', + }); + } + + await this.userRepo.updateAvatar(userId, avatarUrl); + + await this.userRepo.logActivity({ + id: createId(), + userId, + eventType: 'AVATAR_CHANGED', + metadata: { url: avatarUrl }, + }); + + return { + success: true, + avatarUrl, + }; + }; +} diff --git a/src/shared/adapters/mail/adapter.ts b/src/shared/adapters/mail/adapter.ts new file mode 100644 index 0000000..eadbdf9 --- /dev/null +++ b/src/shared/adapters/mail/adapter.ts @@ -0,0 +1,56 @@ +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 { IMailPort } from './port'; + +@Injectable() +export class MailAdapter implements IMailPort { + 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 async sendMail(to: string, subject: string, templateName: string, context: any) { + 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/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/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', + 'Произошла критическая ошибка на стороне сервера', + ), + ); +}; diff --git a/src/shared/decorators/index.ts b/src/shared/decorators/index.ts new file mode 100644 index 0000000..c2f9d19 --- /dev/null +++ b/src/shared/decorators/index.ts @@ -0,0 +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..5a8e94b --- /dev/null +++ b/src/shared/dtos/index.ts @@ -0,0 +1,2 @@ +export * from './pagination.dto'; +export * from './response.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/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/src/shared/entities/index.ts b/src/shared/entities/index.ts index 9d8074b..2e1f6bc 100644 --- a/src/shared/entities/index.ts +++ b/src/shared/entities/index.ts @@ -1 +1,3 @@ export { baseSchema } from './schema'; +export * from '../../modules/user/entities'; +export * from '../../modules/auth/entities'; diff --git a/src/shared/error/filter.ts b/src/shared/error/filter.ts new file mode 100644 index 0000000..d571387 --- /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).send(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..29def94 --- /dev/null +++ b/src/shared/error/swagger.ts @@ -0,0 +1,53 @@ +import { ApiResponse, getSchemaPath } from '@nestjs/swagger'; +import { GlobalErrorResponse } from './schema'; +import { applyDecorators } from '@nestjs/common'; + +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', + }, + }, + }, + }); +}; + +export const ApiBadRequest = (description: string = 'Некорректный запрос') => + applyDecorators(ApiErrorResponse(400, 'BAD_REQUEST', description)); + +export const ApiUnauthorized = (description: string = 'Сессия истекла или токен не валиден') => + applyDecorators(ApiErrorResponse(401, 'AUTH_REQUIRED', description)); + +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)); + +export const ApiConflict = (description: string = 'Ресурс уже существует') => + applyDecorators(ApiErrorResponse(409, 'CONFLICT', description)); 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'; 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); + } + } +} 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; + } +} 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); + }; +} diff --git a/templates/confirmation.hbs b/templates/confirmation.hbs new file mode 100644 index 0000000..c30923b --- /dev/null +++ b/templates/confirmation.hbs @@ -0,0 +1,53 @@ + + + + + + + +
+
+

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..1fa520e --- /dev/null +++ b/templates/reset-password.hbs @@ -0,0 +1,52 @@ + + + + + + + +
+
+

Task Tracker

+
+
+

Сброс пароля

+

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

+

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

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

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

+
+ +
+ + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index de35319..4f21469 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,8 +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/health": ["./libs/health/src"], + "@libs/health/*": ["./libs/health/src/*"], + "@libs/s3": ["./libs/s3/src"], + "@libs/s3/*": ["./libs/s3/src/*"] }, "baseUrl": "./" },