diff --git a/apps/api/package.json b/apps/api/package.json index 5e6d31b..d3ece62 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -12,10 +12,14 @@ }, "dependencies": { "@eden/database": "workspace:*", + "@eden/domain-shared": "workspace:*", "@nestjs/common": "^10.4.15", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", + "@nestjs/swagger": "^8.1.0", "@nestjs/terminus": "^10.2.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", "express": "^4.21.2", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" diff --git a/apps/api/src/resellers/dto/assign-user-to-reseller.dto.ts b/apps/api/src/resellers/dto/assign-user-to-reseller.dto.ts new file mode 100644 index 0000000..f62074a --- /dev/null +++ b/apps/api/src/resellers/dto/assign-user-to-reseller.dto.ts @@ -0,0 +1,15 @@ +import { IsUUID, ValidateIf } from "class-validator"; + +/** + * `resellerId` is required-but-nullable (unassign is expressed as an + * explicit `null`, not by omitting the field) — @ValidateIf lets a null + * value through while still requiring a valid UUID whenever it's present. + */ +export class AssignUserToResellerDto { + @IsUUID() + userId!: string; + + @ValidateIf((_object, value) => value !== null) + @IsUUID() + resellerId!: string | null; +} diff --git a/apps/api/src/resellers/dto/create-reseller.dto.ts b/apps/api/src/resellers/dto/create-reseller.dto.ts new file mode 100644 index 0000000..8603a99 --- /dev/null +++ b/apps/api/src/resellers/dto/create-reseller.dto.ts @@ -0,0 +1,11 @@ +import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from "class-validator"; + +export class CreateResellerDto { + @IsString() + @MinLength(1) + name!: string; + + @IsOptional() @IsString() document?: string; + @IsOptional() @IsEmail() contactEmail?: string; + @IsOptional() @IsBoolean() active?: boolean; +} diff --git a/apps/api/src/resellers/dto/update-reseller.dto.ts b/apps/api/src/resellers/dto/update-reseller.dto.ts new file mode 100644 index 0000000..6e5a84a --- /dev/null +++ b/apps/api/src/resellers/dto/update-reseller.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/swagger"; +import { CreateResellerDto } from "./create-reseller.dto.js"; + +export class UpdateResellerDto extends PartialType(CreateResellerDto) {} diff --git a/apps/api/src/resellers/resellers.controller.ts b/apps/api/src/resellers/resellers.controller.ts new file mode 100644 index 0000000..704dc07 --- /dev/null +++ b/apps/api/src/resellers/resellers.controller.ts @@ -0,0 +1,82 @@ +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { ResellersService } from "./resellers.service.js"; +import { CreateResellerDto } from "./dto/create-reseller.dto.js"; +import { UpdateResellerDto } from "./dto/update-reseller.dto.js"; +import { AssignUserToResellerDto } from "./dto/assign-user-to-reseller.dto.js"; +import { JwtAuthGuard } from "../auth/jwt-auth.guard.js"; +import { PermissionGuard } from "../auth/permission.guard.js"; +import { RequirePermission } from "../auth/decorators/require-permission.decorator.js"; +import { CurrentUser } from "../auth/decorators/current-user.decorator.js"; +import type { EdenAuthenticatedUser } from "../auth/jwt-auth.guard.js"; + +/** + * Operational reseller CRUD (eden.md §1.7, §7) — MVP scope only. The full + * "Programa de Canais" approval workflow with public tokens (eden.md §6) + * is a separate, later phase and isn't touched here. + * + * Read is open to any authenticated user (no @RequirePermission) — + * mirrors Companies: needed for reseller-selector dropdowns elsewhere + * (e.g. Orçamentos, cadastro de usuário) and isn't sensitive data. + * Write requires `resellers:edit`. + * + * Deliberate deviation from legacy, noted per eden-commercial rules: + * the legacy (`resellers.routes.js`, eden.md §7) hardcoded this whole + * CRUD to `requireAdmin` (a role check — admin/super_admin only, + * stricter than the revenda cadastro/aprovação feature). EDEN + * generalizes that role check to the resource+action+scope permission + * model (docs/adr/0004-permission-model.md) instead, so a role can be + * granted `resellers:edit` without needing the full `admin` role. + */ +@ApiTags("resellers") +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, PermissionGuard) +@Controller("v1/resellers") +export class ResellersController { + constructor(private readonly resellers: ResellersService) {} + + @Get() + list(@Query("includeInactive") includeInactive?: string) { + return this.resellers.list(includeInactive === "true"); + } + + @Get(":id") + get(@Param("id") id: string) { + return this.resellers.get(id); + } + + @Post() + @RequirePermission("resellers", "edit") + create(@Body() dto: CreateResellerDto, @CurrentUser() user: EdenAuthenticatedUser) { + return this.resellers.create(dto, user.id); + } + + @Patch(":id") + @RequirePermission("resellers", "edit") + update( + @Param("id") id: string, + @Body() dto: UpdateResellerDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.resellers.update(id, dto, user.id); + } + + /** + * Legacy (eden.md §7) exposed reseller↔user assignment as a + * users-domain route: `PATCH /users/:id { reseller_id }`. EDEN + * deliberately places the same operation here instead — documented + * deviation — so it stays gated by `resellers:edit` (matching the + * legacy's `requireAdmin` scope for this screen) rather than needing a + * separate `users:edit` grant just to manage a reseller's roster from + * the Resellers screen. + */ + @Patch(":id/assign-user") + @RequirePermission("resellers", "edit") + assignUser( + @Param("id") id: string, + @Body() dto: AssignUserToResellerDto, + @CurrentUser() user: EdenAuthenticatedUser, + ) { + return this.resellers.assignUser(id, dto, user.id); + } +} diff --git a/apps/api/src/resellers/resellers.module.ts b/apps/api/src/resellers/resellers.module.ts new file mode 100644 index 0000000..91ddd9f --- /dev/null +++ b/apps/api/src/resellers/resellers.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { AuthModule } from "../auth/auth.module.js"; +import { ResellersController } from "./resellers.controller.js"; +import { ResellersService } from "./resellers.service.js"; + +@Module({ + imports: [AuthModule], + controllers: [ResellersController], + providers: [ResellersService], +}) +export class ResellersModule {} diff --git a/apps/api/src/resellers/resellers.service.ts b/apps/api/src/resellers/resellers.service.ts new file mode 100644 index 0000000..58dd2ef --- /dev/null +++ b/apps/api/src/resellers/resellers.service.ts @@ -0,0 +1,101 @@ +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { resellersRepo, usersRepo, auditRepo, type Reseller } from "@eden/database"; +import { onlyDigits, isValidCPF, isValidCNPJ } from "@eden/domain-shared"; +import type { CreateResellerDto } from "./dto/create-reseller.dto.js"; +import type { UpdateResellerDto } from "./dto/update-reseller.dto.js"; +import type { AssignUserToResellerDto } from "./dto/assign-user-to-reseller.dto.js"; + +@Injectable() +export class ResellersService { + list(includeInactive: boolean): Promise { + return resellersRepo.listResellers(includeInactive); + } + + async get(id: string): Promise { + const reseller = await resellersRepo.getResellerById(id); + if (!reseller) throw new NotFoundException("Revenda não encontrada"); + return reseller; + } + + async create(dto: CreateResellerDto, actorId: string): Promise { + this.assertValidDocumentIfPresent(dto.document); + + const reseller = await resellersRepo.createReseller({ + name: dto.name, + document: dto.document ? onlyDigits(dto.document) : undefined, + contactEmail: dto.contactEmail, + active: dto.active, + createdBy: actorId, + }); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId, + action: "reseller.created", + resourceType: "reseller", + resourceId: reseller.id, + }); + + return reseller; + } + + async update(id: string, dto: UpdateResellerDto, actorId: string): Promise { + await this.get(id); // 404 if missing + this.assertValidDocumentIfPresent(dto.document); + + const reseller = await resellersRepo.updateReseller(id, { + name: dto.name, + document: dto.document ? onlyDigits(dto.document) : undefined, + contactEmail: dto.contactEmail, + active: dto.active, + }); + if (!reseller) throw new NotFoundException("Revenda não encontrada"); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId, + action: "reseller.updated", + resourceType: "reseller", + resourceId: id, + metadata: { fields: Object.keys(dto) }, + }); + + return reseller; + } + + async assignUser( + id: string, + dto: AssignUserToResellerDto, + actorId: string, + ): Promise<{ userId: string; resellerId: string | null }> { + await this.get(id); // 404 if the reseller in the URL doesn't exist + if (dto.resellerId !== null && dto.resellerId !== id) { + await this.get(dto.resellerId); // 404 if a different target reseller doesn't exist + } + + const found = await usersRepo.assignUserToReseller(dto.userId, dto.resellerId); + if (!found) throw new NotFoundException("Usuário não encontrado"); + + await auditRepo.appendAuditEvent({ + actorType: "user", + actorId, + action: "reseller.user_assigned", + resourceType: "user", + resourceId: dto.userId, + metadata: { resellerId: dto.resellerId }, + }); + + return { userId: dto.userId, resellerId: dto.resellerId }; + } + + /** Mirrors Companies' assertValidCnpjIfPresent — document is CPF or CNPJ here. */ + private assertValidDocumentIfPresent(document: string | undefined): void { + if (!document) return; + const digits = onlyDigits(document); + const valid = + digits.length === 11 ? isValidCPF(digits) : digits.length === 14 ? isValidCNPJ(digits) : false; + if (!valid) { + throw new BadRequestException("CPF/CNPJ inválido"); + } + } +} diff --git a/packages/database/migrations/1788439142825_resellers.cjs b/packages/database/migrations/1788439142825_resellers.cjs new file mode 100644 index 0000000..28eccaf --- /dev/null +++ b/packages/database/migrations/1788439142825_resellers.cjs @@ -0,0 +1,59 @@ +/** + * Commercial bounded context, first slice: `resellers` — the operational + * reseller/partner registry (eden.md §1.7). Pre-dates the "Programa de + * Canais" approval workflow with public tokens described in eden.md §6 — + * that workflow (client_registrations/reseller_registrations, public + * approval tokens) is explicitly out of scope for this migration and + * lands in its own later phase. This is just the simple operational CRUD + * table eden.md §1.7 describes, already referenced by `quotes.reseller_id` + * conceptually (quotes themselves aren't modeled yet either — Commercial + * phase lands module by module). + * + * Also adds `users.reseller_id` (nullable) — how a user gets scoped to a + * reseller, per eden.md §7 and the `scope=reseller` permission model + * (docs/adr/0004-permission-model.md). Lives in this migration rather + * than a separate one so the FK to `resellers(id)` can be added in the + * same statement the table is created in, without a two-step migration. + */ + +exports.shorthands = undefined; + +exports.up = (pgm) => { + pgm.sql(` + CREATE TABLE resellers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by UUID REFERENCES users(id), + name TEXT NOT NULL, + document TEXT, + contact_email TEXT, + active BOOLEAN NOT NULL DEFAULT true + ); + COMMENT ON TABLE resellers IS + 'Cadastro operacional de revenda — eden.md §1.7. document é CPF/CNPJ livre (sem CHECK de dígito no banco, validado na camada de aplicação, igual companies.cnpj). Leitura aberta a qualquer autenticado (usado em seletores, ex. Orçamentos); escrita restrita à permissão resellers:edit.'; + + CREATE TRIGGER trg_resellers_updated_at BEFORE UPDATE ON resellers + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + + CREATE INDEX idx_resellers_active ON resellers(active); + + -- ===================================================================== + -- users.reseller_id — scopes a user to a reseller (eden.md §7, + -- docs/adr/0004-permission-model.md scope=reseller) + -- ===================================================================== + ALTER TABLE users ADD COLUMN reseller_id UUID REFERENCES resellers(id); + COMMENT ON COLUMN users.reseller_id IS + 'Nullable — vincula o usuário a uma revenda (vendedor de revenda), escopando o que ele vê (eden.md §7). Atribuído via PATCH /v1/resellers/:id/assign-user, nunca aceito de campo vindo do cliente para decisão de autorização (docs/architecture.md, ADR-0004).'; + + CREATE INDEX idx_users_reseller ON users(reseller_id); + `); +}; + +exports.down = (pgm) => { + pgm.sql(` + DROP INDEX IF EXISTS idx_users_reseller; + ALTER TABLE users DROP COLUMN IF EXISTS reseller_id; + DROP TABLE IF EXISTS resellers; + `); +}; diff --git a/packages/database/src/repositories/audit.ts b/packages/database/src/repositories/audit.ts new file mode 100644 index 0000000..b1d6387 --- /dev/null +++ b/packages/database/src/repositories/audit.ts @@ -0,0 +1,66 @@ +import { createHash } from "node:crypto"; +import { query } from "../pool.js"; + +export interface AuditEventInput { + actorType: "user" | "system" | "service"; + actorId?: string | null; + action: string; + resourceType: string; + resourceId?: string | null; + metadata?: Record; + ipAddress?: string | null; +} + +function canonicalJson(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + const obj = value as Record; + const keys = Object.keys(obj).sort(); + return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`; +} + +/** + * Appends one event to the audit_log hash chain (see baseline-schema + * migration and .claude/skills/eden-security/references/audit.md). Never + * call this inside a larger transaction that might roll back for + * unrelated reasons without considering that the chain must stay + * contiguous — a rolled-back event never happened (fine, nothing was + * recorded), but a *committed* event must never be skipped when + * computing previous_event_hash for the next one. + */ +export async function appendAuditEvent(input: AuditEventInput): Promise { + const { rows } = await query<{ event_hash: string }>( + "SELECT event_hash FROM audit_log ORDER BY seq DESC LIMIT 1", + ); + const previousEventHash = rows[0]?.event_hash ?? ""; + + const fields = { + actorType: input.actorType, + actorId: input.actorId ?? null, + action: input.action, + resourceType: input.resourceType, + resourceId: input.resourceId ?? null, + metadata: input.metadata ?? {}, + ipAddress: input.ipAddress ?? null, + }; + const eventHash = createHash("sha256") + .update(previousEventHash + canonicalJson(fields)) + .digest("hex"); + + await query( + `INSERT INTO audit_log + (actor_type, actor_id, action, resource_type, resource_id, metadata, ip_address, previous_event_hash, event_hash) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, + [ + input.actorType, + input.actorId ?? null, + input.action, + input.resourceType, + input.resourceId ?? null, + JSON.stringify(input.metadata ?? {}), + input.ipAddress ?? null, + previousEventHash || null, + eventHash, + ], + ); +} diff --git a/packages/database/src/repositories/resellers.ts b/packages/database/src/repositories/resellers.ts new file mode 100644 index 0000000..2441a79 --- /dev/null +++ b/packages/database/src/repositories/resellers.ts @@ -0,0 +1,71 @@ +import { query } from "../pool.js"; + +export interface Reseller { + id: string; + created_at: Date; + updated_at: Date; + created_by: string | null; + name: string; + document: string | null; + contact_email: string | null; + active: boolean; +} + +export async function listResellers(includeInactive = false): Promise { + const { rows } = await query( + includeInactive + ? "SELECT * FROM resellers ORDER BY created_at ASC" + : "SELECT * FROM resellers WHERE active = true ORDER BY created_at ASC", + ); + return rows; +} + +export async function getResellerById(id: string): Promise { + const { rows } = await query("SELECT * FROM resellers WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export interface CreateResellerInput { + name: string; + document?: string | null; + contactEmail?: string | null; + active?: boolean; + createdBy?: string | null; +} + +export async function createReseller(input: CreateResellerInput): Promise { + const { rows } = await query( + `INSERT INTO resellers (name, document, contact_email, active, created_by) + VALUES ($1, $2, $3, COALESCE($4, true), $5) + RETURNING *`, + [ + input.name, + input.document ?? null, + input.contactEmail ?? null, + input.active ?? null, + input.createdBy ?? null, + ], + ); + const reseller = rows[0]; + if (!reseller) throw new Error("failed to create reseller"); + return reseller; +} + +export type UpdateResellerInput = Partial>; + +export async function updateReseller( + id: string, + input: UpdateResellerInput, +): Promise { + const { rows } = await query( + `UPDATE resellers SET + name = COALESCE($2, name), + document = COALESCE($3, document), + contact_email = COALESCE($4, contact_email), + active = COALESCE($5, active) + WHERE id = $1 + RETURNING *`, + [id, input.name ?? null, input.document ?? null, input.contactEmail ?? null, input.active ?? null], + ); + return rows[0] ?? null; +} diff --git a/packages/database/src/repositories/users.ts b/packages/database/src/repositories/users.ts new file mode 100644 index 0000000..1dabe1e --- /dev/null +++ b/packages/database/src/repositories/users.ts @@ -0,0 +1,30 @@ +import { query } from "../pool.js"; + +/** + * NOTE: at the time this file was created, the fuller `users` repository + * (createUser, findUserByEmail, findUserById, etc. — identity/auth + * bounded context) hadn't landed in this worktree yet, since it's being + * built in parallel by another agent. Per the resellers-module task + * spec this function was meant to be appended to the end of that + * existing file rather than living in a file of its own — do that at + * merge time; this file's only reason to exist standalone right now is + * that the one it was supposed to be appended to didn't exist here. + */ + +/** + * Reseller-scoping: sets or clears which reseller a user belongs to + * (`users.reseller_id`, nullable — eden.md §1.7, §7). Resolved and + * enforced server-side only; callers must never accept `reseller_id` + * from client input for authorization decisions (docs/adr/0004, + * docs/architecture.md). + */ +export async function assignUserToReseller( + userId: string, + resellerId: string | null, +): Promise { + const { rowCount } = await query("UPDATE users SET reseller_id = $2 WHERE id = $1", [ + userId, + resellerId, + ]); + return (rowCount ?? 0) > 0; +} diff --git a/packages/domain-shared/src/index.ts b/packages/domain-shared/src/index.ts index 1d722dc..d0960c6 100644 --- a/packages/domain-shared/src/index.ts +++ b/packages/domain-shared/src/index.ts @@ -5,3 +5,4 @@ * bounded context needs to share a type with the first. */ export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0"; +export { onlyDigits, isValidCPF, isValidCNPJ } from "./validators/br-documents.js"; diff --git a/packages/domain-shared/src/validators/br-documents.ts b/packages/domain-shared/src/validators/br-documents.ts new file mode 100644 index 0000000..46c0202 --- /dev/null +++ b/packages/domain-shared/src/validators/br-documents.ts @@ -0,0 +1,50 @@ +/** + * CPF/CNPJ format validation (digit-check only — never confirms a document + * actually exists or belongs to whoever claims it). Ported from the + * standard Brazilian algorithm, matching the legacy's isValidCPF/ + * isValidCNPJ (eden.md §1 companies, §3 client registration, §1.7 + * resellers). + */ +export function onlyDigits(value: string): string { + return value.replace(/\D/g, ""); +} + +export function isValidCPF(value: string): boolean { + const cpf = onlyDigits(value); + if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false; + + const digits = cpf.split("").map(Number); + const checkDigit = (length: number): number => { + let sum = 0; + for (let i = 0; i < length; i++) { + sum += digits[i]! * (length + 1 - i); + } + const remainder = (sum * 10) % 11; + return remainder === 10 ? 0 : remainder; + }; + + return checkDigit(9) === digits[9] && checkDigit(10) === digits[10]; +} + +export function isValidCNPJ(value: string): boolean { + const cnpj = onlyDigits(value); + if (cnpj.length !== 14 || /^(\d)\1{13}$/.test(cnpj)) return false; + + const digits = cnpj.split("").map(Number); + // Standard CNPJ weight sequences — the second is the first with a + // leading 6, not a generic slice (easy to get subtly wrong otherwise). + const WEIGHTS_12 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + const WEIGHTS_13 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + const checkDigit = (length: number, weights: number[]): number => { + let sum = 0; + for (let i = 0; i < length; i++) { + sum += digits[i]! * weights[i]!; + } + const remainder = sum % 11; + return remainder < 2 ? 0 : 11 - remainder; + }; + + return ( + checkDigit(12, WEIGHTS_12) === digits[12] && checkDigit(13, WEIGHTS_13) === digits[13] + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72ac0ae..79045cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,18 +20,30 @@ importers: '@eden/database': specifier: workspace:* version: link:../../packages/database + '@eden/domain-shared': + specifier: workspace:* + version: link:../../packages/domain-shared '@nestjs/common': specifier: ^10.4.15 - version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + version: 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@nestjs/core': specifier: ^10.4.15 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^10.4.15 - version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) + version: 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) + '@nestjs/swagger': + specifier: ^8.1.0 + version: 8.1.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) '@nestjs/terminus': specifier: ^10.2.3 - version: 10.3.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 10.3.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.4 express: specifier: ^4.21.2 version: 4.22.2(supports-color@7.2.0) @@ -672,6 +684,9 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} engines: {node: ^22.20 || ^24.12 || >=25} @@ -709,12 +724,42 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/mapped-types@2.0.6': + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/platform-express@10.4.22': resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 + '@nestjs/swagger@8.1.1': + resolution: {integrity: sha512-5Mda7H1DKnhKtlsb0C7PYshcvILv8UFyUotHzxmWh0G65Z21R3LZH/J8wmpnlzL4bmXIfr42YwbEwRxgzpJ5sQ==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/terminus@10.3.0': resolution: {integrity: sha512-vOJGCwt1OgrFuuxWQwPoaHqy9m9CfIk2qMUX2mosZLK5dFVJSEjHXrklkh3/Fw9PiUnfzvYFfiAdJRzUaxx+5Q==} peerDependencies: @@ -921,6 +966,9 @@ packages: cpu: [x64] os: [win32] + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -1017,6 +1065,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -1051,6 +1102,9 @@ packages: arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -1141,6 +1195,12 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.4: + resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==} + cli-boxes@2.2.1: resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} engines: {node: '>=6'} @@ -1453,6 +1513,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1463,6 +1527,9 @@ packages: engines: {node: '>=6'} hasBin: true + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1470,6 +1537,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -1904,6 +1974,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} @@ -1994,6 +2067,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2373,10 +2450,12 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.15.1': {} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true - '@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': + '@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)': dependencies: file-type: 20.4.1(supports-color@7.2.0) iterare: 1.2.1 @@ -2384,12 +2463,15 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 transitivePeerDependencies: - supports-color - '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -2399,14 +2481,22 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) + '@nestjs/platform-express': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0) transitivePeerDependencies: - encoding - '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)': + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + + '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(supports-color@7.2.0)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) body-parser: 1.20.4(supports-color@7.2.0) cors: 2.8.5 express: 4.22.1(supports-color@7.2.0) @@ -2415,10 +2505,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/terminus@10.3.0(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/swagger@8.1.1(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.18.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.4 + + '@nestjs/terminus@10.3.0(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0) + '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/platform-express@10.4.22)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 @@ -2521,6 +2626,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.63.1': optional: true + '@scarf/scarf@1.4.0': {} + '@tokenizer/inflate@0.2.7(supports-color@7.2.0)': dependencies: debug: 4.4.3(supports-color@7.2.0) @@ -2630,6 +2737,8 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 22.20.1 + '@types/validator@13.15.10': {} + '@vitejs/plugin-react@4.7.0(supports-color@7.2.0)(vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13))': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) @@ -2668,6 +2777,8 @@ snapshots: arg@5.0.2: {} + argparse@2.0.1: {} + array-flatten@1.1.1: {} autoprefixer@10.5.4(postcss@8.5.26): @@ -2789,6 +2900,14 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + class-transformer@0.5.1: {} + + class-validator@0.14.4: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.13.12 + validator: 13.15.35 + cli-boxes@2.2.1: {} cliui@8.0.1: @@ -3177,14 +3296,22 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + jsesc@3.1.0: {} json5@2.2.3: {} + libphonenumber-js@1.13.12: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} + lodash@4.17.21: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -3607,6 +3734,10 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + tailwindcss@3.4.19(tsx@4.23.13): dependencies: '@alloc/quick-lru': 5.3.0 @@ -3712,6 +3843,8 @@ snapshots: utils-merge@1.0.1: {} + validator@13.15.35: {} + vary@1.1.2: {} vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b1beebc..ad838df 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,4 +3,5 @@ packages: - "packages/*" allowBuilds: '@nestjs/core': true + '@scarf/scarf': false esbuild: true