Auth completo (Argon2id + JWT/refresh rotativo), permissões, Companies e OpenAPI
- Auth (apps/api/src/auth/): Argon2id com parâmetros versionados
(ADR-0005), access token JWT de 15min, refresh token opaco de 256 bits
com rotação e detecção de replay (reuso de token já rotacionado revoga
toda a cadeia de sessões do usuário). Endpoints /api/v1/auth/
{login,refresh,logout,me}.
- Permissões resource+action+scope (ADR-0004) via PermissionGuard,
resolvidas do banco a cada request (nunca cacheadas no JWT) — mudança
de papel tem efeito imediato. super_admin com bypass hardcoded,
espelhando a trava já existente no banco.
- Companies (migration + CRUD): leitura aberta a qualquer autenticado,
escrita restrita a companies:manage; validação de CNPJ via
packages/domain-shared (dígito verificador).
- SeedService: cria o primeiro super_admin a partir de
EDEN_SUPERADMIN_EMAIL/PASSWORD só quando `users` está vazia.
- OpenAPI/Swagger em /api/docs; ValidationPipe global com
whitelist+forbidNonWhitelisted (proteção contra mass-assignment).
- packages/database: repositórios (roles/users/sessions/audit/companies)
reutilizáveis por api e worker.
Validado manualmente contra a stack real (não só typecheck): login com
senha errada → 401 genérico; refresh rotaciona e invalida o token
anterior; reuso do token antigo → replay detectado, cadeia revogada;
logout idempotente; audit log com a sequência completa de eventos;
CNPJ inválido rejeitado; mass-assignment rejeitado.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,17 +11,25 @@
|
|||||||
"test": "echo 'no tests yet'"
|
"test": "echo 'no tests yet'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@eden/config": "workspace:*",
|
||||||
"@eden/database": "workspace:*",
|
"@eden/database": "workspace:*",
|
||||||
|
"@eden/domain-shared": "workspace:*",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
"@nestjs/platform-express": "^10.4.15",
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
|
"@nestjs/swagger": "^8.1.0",
|
||||||
"@nestjs/terminus": "^10.2.3",
|
"@nestjs/terminus": "^10.2.3",
|
||||||
|
"argon2": "^0.41.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TerminusModule } from "@nestjs/terminus";
|
import { TerminusModule } from "@nestjs/terminus";
|
||||||
import { HealthController } from "./health/health.controller";
|
import { HealthController } from "./health/health.controller.js";
|
||||||
|
import { ConfigModule } from "./config/config.module.js";
|
||||||
|
import { AuthModule } from "./auth/auth.module.js";
|
||||||
|
import { BootstrapModule } from "./bootstrap/bootstrap.module.js";
|
||||||
|
import { CompaniesModule } from "./companies/companies.module.js";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TerminusModule],
|
imports: [ConfigModule, TerminusModule, AuthModule, BootstrapModule, CompaniesModule],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
47
apps/api/src/auth/auth.controller.ts
Normal file
47
apps/api/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, Post, Req, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||||
|
import type { Request } from "express";
|
||||||
|
import { AuthService } from "./auth.service.js";
|
||||||
|
import { LoginDto } from "./dto/login.dto.js";
|
||||||
|
import { RefreshDto } from "./dto/refresh.dto.js";
|
||||||
|
import { JwtAuthGuard } from "./jwt-auth.guard.js";
|
||||||
|
import { CurrentUser } from "./decorators/current-user.decorator.js";
|
||||||
|
import type { EdenAuthenticatedUser } from "./jwt-auth.guard.js";
|
||||||
|
|
||||||
|
function requestContext(req: Request) {
|
||||||
|
return {
|
||||||
|
ipAddress: req.ip ?? null,
|
||||||
|
userAgent: req.headers["user-agent"] ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags("auth")
|
||||||
|
@Controller("v1/auth")
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly auth: AuthService) {}
|
||||||
|
|
||||||
|
@Post("login")
|
||||||
|
@HttpCode(200)
|
||||||
|
async login(@Body() dto: LoginDto, @Req() req: Request) {
|
||||||
|
return this.auth.login(dto.email, dto.password, dto.applicationKey, requestContext(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("refresh")
|
||||||
|
@HttpCode(200)
|
||||||
|
async refresh(@Body() dto: RefreshDto, @Req() req: Request) {
|
||||||
|
return this.auth.refresh(dto.refreshToken, requestContext(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("logout")
|
||||||
|
@HttpCode(204)
|
||||||
|
async logout(@Body() dto: RefreshDto, @Req() req: Request) {
|
||||||
|
await this.auth.logout(dto.refreshToken, requestContext(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("me")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async me(@CurrentUser() user: EdenAuthenticatedUser) {
|
||||||
|
return this.auth.getProfile(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
apps/api/src/auth/auth.module.ts
Normal file
14
apps/api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthController } from "./auth.controller.js";
|
||||||
|
import { AuthService } from "./auth.service.js";
|
||||||
|
import { PasswordService } from "./password.service.js";
|
||||||
|
import { TokenService } from "./token.service.js";
|
||||||
|
import { JwtAuthGuard } from "./jwt-auth.guard.js";
|
||||||
|
import { PermissionGuard } from "./permission.guard.js";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, PasswordService, TokenService, JwtAuthGuard, PermissionGuard],
|
||||||
|
exports: [PasswordService, TokenService, JwtAuthGuard, PermissionGuard],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
181
apps/api/src/auth/auth.service.ts
Normal file
181
apps/api/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { ForbiddenException, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import { usersRepo, sessionsRepo, auditRepo, type User } from "@eden/database";
|
||||||
|
import { PasswordService } from "./password.service.js";
|
||||||
|
import { TokenService } from "./token.service.js";
|
||||||
|
import { resolvePermissions } from "./permissions.js";
|
||||||
|
|
||||||
|
export interface RequestContext {
|
||||||
|
ipAddress?: string | null;
|
||||||
|
userAgent?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResult {
|
||||||
|
accessToken: string;
|
||||||
|
expiresIn: number;
|
||||||
|
refreshToken: string;
|
||||||
|
user: PublicUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string | null;
|
||||||
|
roleKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPublicUser(user: User): PublicUser {
|
||||||
|
return { id: user.id, email: user.email, fullName: user.full_name, roleKey: user.role_key };
|
||||||
|
}
|
||||||
|
|
||||||
|
const GENERIC_LOGIN_ERROR = "Credenciais inválidas";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly passwords: PasswordService,
|
||||||
|
private readonly tokens: TokenService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async login(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
applicationKey: "core" | "reseller" | "subscriber",
|
||||||
|
ctx: RequestContext,
|
||||||
|
): Promise<AuthResult> {
|
||||||
|
const user = await usersRepo.findUserByEmail(email);
|
||||||
|
|
||||||
|
// Same generic error whether the account doesn't exist or the password
|
||||||
|
// is wrong — anti-enumeration, mirrors eden.md §4.1.
|
||||||
|
if (!user || !user.password_hash) {
|
||||||
|
throw new UnauthorizedException(GENERIC_LOGIN_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await this.passwords.verify(user.password_hash, password);
|
||||||
|
if (!valid) {
|
||||||
|
throw new UnauthorizedException(GENERIC_LOGIN_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.passwords.needsRehash(user.password_algo_version)) {
|
||||||
|
const { hash, algoVersion } = await this.passwords.hash(password);
|
||||||
|
await usersRepo.updateUserPassword(user.id, hash, algoVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grantedApps = await usersRepo.getUserApplications(user.id);
|
||||||
|
if (!grantedApps.includes(applicationKey)) {
|
||||||
|
throw new ForbiddenException("Usuário sem acesso a esta aplicação");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.issueTokens(user, applicationKey, ctx);
|
||||||
|
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "user",
|
||||||
|
actorId: user.id,
|
||||||
|
action: "auth.login",
|
||||||
|
resourceType: "user",
|
||||||
|
resourceId: user.id,
|
||||||
|
metadata: { applicationKey },
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(refreshTokenPlaintext: string, ctx: RequestContext): Promise<AuthResult> {
|
||||||
|
const hash = this.tokens.hashRefreshToken(refreshTokenPlaintext);
|
||||||
|
const active = await sessionsRepo.findActiveSessionByRefreshTokenHash(hash);
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
// Replay detection: a token that exists but is revoked/expired being
|
||||||
|
// reused is a signal the refresh token chain may be compromised —
|
||||||
|
// revoke every session of that user defensively (ADR-0003).
|
||||||
|
const anyState = await sessionsRepo.findSessionByRefreshTokenHashAnyState(hash);
|
||||||
|
if (anyState?.revoked_at) {
|
||||||
|
await sessionsRepo.revokeAllUserSessions(anyState.user_id);
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "system",
|
||||||
|
actorId: anyState.user_id,
|
||||||
|
action: "auth.refresh_replay_detected",
|
||||||
|
resourceType: "session",
|
||||||
|
resourceId: anyState.id,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new UnauthorizedException("Sessão inválida ou expirada");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await usersRepo.findUserById(active.user_id);
|
||||||
|
if (!user || !user.is_active) {
|
||||||
|
throw new UnauthorizedException("Sessão inválida ou expirada");
|
||||||
|
}
|
||||||
|
|
||||||
|
await sessionsRepo.touchSession(active.id);
|
||||||
|
const result = await this.issueTokens(user, active.application_key, ctx, active.id);
|
||||||
|
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "user",
|
||||||
|
actorId: user.id,
|
||||||
|
action: "auth.refresh",
|
||||||
|
resourceType: "session",
|
||||||
|
resourceId: active.id,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(refreshTokenPlaintext: string, ctx: RequestContext): Promise<void> {
|
||||||
|
const hash = this.tokens.hashRefreshToken(refreshTokenPlaintext);
|
||||||
|
const session = await sessionsRepo.findActiveSessionByRefreshTokenHash(hash);
|
||||||
|
if (!session) return; // idempotent — already logged out
|
||||||
|
await sessionsRepo.revokeSession(session.id);
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "user",
|
||||||
|
actorId: session.user_id,
|
||||||
|
action: "auth.logout",
|
||||||
|
resourceType: "session",
|
||||||
|
resourceId: session.id,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProfile(userId: string) {
|
||||||
|
const user = await usersRepo.findUserById(userId);
|
||||||
|
if (!user || !user.is_active) throw new UnauthorizedException();
|
||||||
|
const permissions = await resolvePermissions(user.role_key);
|
||||||
|
const applications = await usersRepo.getUserApplications(user.id);
|
||||||
|
return {
|
||||||
|
user: toPublicUser(user),
|
||||||
|
permissions: permissions.superAdmin
|
||||||
|
? ("ALL" as const)
|
||||||
|
: permissions.grants.map((g) => ({ resource: g.resource, action: g.action, scope: g.scope })),
|
||||||
|
applications,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async issueTokens(
|
||||||
|
user: User,
|
||||||
|
applicationKey: "core" | "reseller" | "subscriber",
|
||||||
|
ctx: RequestContext,
|
||||||
|
replacesSessionId?: string,
|
||||||
|
): Promise<AuthResult> {
|
||||||
|
const access = this.tokens.signAccessToken({ sub: user.id, app: applicationKey });
|
||||||
|
const refresh = this.tokens.generateRefreshToken();
|
||||||
|
|
||||||
|
await sessionsRepo.createSession({
|
||||||
|
userId: user.id,
|
||||||
|
applicationKey,
|
||||||
|
refreshTokenHash: refresh.hash,
|
||||||
|
expiresAt: refresh.expiresAt,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userAgent: ctx.userAgent,
|
||||||
|
replacesSessionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: access.token,
|
||||||
|
expiresIn: access.expiresIn,
|
||||||
|
refreshToken: refresh.plaintext,
|
||||||
|
user: toPublicUser(user),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
7
apps/api/src/auth/decorators/current-user.decorator.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||||
|
import type { Request } from "express";
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator((_data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest<Request>();
|
||||||
|
return request.edenUser;
|
||||||
|
});
|
||||||
19
apps/api/src/auth/decorators/require-permission.decorator.ts
Normal file
19
apps/api/src/auth/decorators/require-permission.decorator.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { SetMetadata } from "@nestjs/common";
|
||||||
|
import type { RolePermission } from "@eden/database";
|
||||||
|
|
||||||
|
export const REQUIRE_PERMISSION_KEY = "eden:requirePermission";
|
||||||
|
|
||||||
|
export interface RequiredPermission {
|
||||||
|
resource: string;
|
||||||
|
action: RolePermission["action"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares that an endpoint needs (resource, action) per ADR-0004.
|
||||||
|
* super_admin always passes. Confirms the actor holds *some* scope for
|
||||||
|
* this resource/action — row-level filtering by the granted scope (own
|
||||||
|
* vs. all, etc.) is the responsibility of the service/query layer, which
|
||||||
|
* reads it off `request.edenUser.permissions` (see permissions.ts).
|
||||||
|
*/
|
||||||
|
export const RequirePermission = (resource: string, action: RequiredPermission["action"]) =>
|
||||||
|
SetMetadata(REQUIRE_PERMISSION_KEY, { resource, action } satisfies RequiredPermission);
|
||||||
13
apps/api/src/auth/dto/login.dto.ts
Normal file
13
apps/api/src/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { IsEmail, IsIn, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsEmail()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
password!: string;
|
||||||
|
|
||||||
|
@IsIn(["core", "reseller", "subscriber"])
|
||||||
|
applicationKey!: "core" | "reseller" | "subscriber";
|
||||||
|
}
|
||||||
7
apps/api/src/auth/dto/refresh.dto.ts
Normal file
7
apps/api/src/auth/dto/refresh.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class RefreshDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(32)
|
||||||
|
refreshToken!: string;
|
||||||
|
}
|
||||||
65
apps/api/src/auth/jwt-auth.guard.ts
Normal file
65
apps/api/src/auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
|
import type { Request } from "express";
|
||||||
|
import { usersRepo } from "@eden/database";
|
||||||
|
import { TokenService } from "./token.service.js";
|
||||||
|
import { resolvePermissions, type ResolvedPermissions } from "./permissions.js";
|
||||||
|
|
||||||
|
export interface EdenAuthenticatedUser {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string | null;
|
||||||
|
roleKey: string;
|
||||||
|
applicationKey: "core" | "reseller" | "subscriber";
|
||||||
|
permissions: ResolvedPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "express" {
|
||||||
|
interface Request {
|
||||||
|
edenUser?: EdenAuthenticatedUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the access token's signature/expiry, then re-resolves the user
|
||||||
|
* and their permissions from the database on every request — deliberately
|
||||||
|
* not cached in the JWT payload, so a role/permission change (or account
|
||||||
|
* deactivation) takes effect immediately rather than waiting out the
|
||||||
|
* token's 15-minute lifetime. Mirrors the legacy's LEFT JOIN-on-every-
|
||||||
|
* request philosophy (eden.md §1, §3.3).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard implements CanActivate {
|
||||||
|
constructor(private readonly tokens: TokenService) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const header = request.headers.authorization;
|
||||||
|
if (!header?.startsWith("Bearer ")) {
|
||||||
|
throw new UnauthorizedException("Token de acesso ausente");
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = this.tokens.verifyAccessToken(header.slice("Bearer ".length));
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException("Token de acesso inválido ou expirado");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await usersRepo.findUserById(payload.sub);
|
||||||
|
if (!user || !user.is_active) {
|
||||||
|
throw new UnauthorizedException("Usuário inativo ou inexistente");
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await resolvePermissions(user.role_key);
|
||||||
|
|
||||||
|
request.edenUser = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.full_name,
|
||||||
|
roleKey: user.role_key,
|
||||||
|
applicationKey: payload.app,
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
apps/api/src/auth/password.service.ts
Normal file
44
apps/api/src/auth/password.service.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import * as argon2 from "argon2";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Argon2id password hashing with versioned parameters (ADR-0005). Bumping
|
||||||
|
* CURRENT_ALGO_VERSION and its entry in PARAMS_BY_VERSION lets us raise
|
||||||
|
* cost over time without invalidating existing hashes — verification
|
||||||
|
* always uses the parameters embedded in argon2's own encoded hash
|
||||||
|
* string, and a stored user is only re-hashed with the new parameters on
|
||||||
|
* their next successful login (see needsRehash()).
|
||||||
|
*/
|
||||||
|
const PARAMS_BY_VERSION: Record<number, argon2.Options> = {
|
||||||
|
1: {
|
||||||
|
type: argon2.argon2id,
|
||||||
|
memoryCost: 19456, // 19 MiB — OWASP minimum recommendation for argon2id
|
||||||
|
timeCost: 2,
|
||||||
|
parallelism: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CURRENT_ALGO_VERSION = 1;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordService {
|
||||||
|
async hash(plain: string): Promise<{ hash: string; algoVersion: number }> {
|
||||||
|
const params = PARAMS_BY_VERSION[CURRENT_ALGO_VERSION];
|
||||||
|
const hash = await argon2.hash(plain, params);
|
||||||
|
return { hash, algoVersion: CURRENT_ALGO_VERSION };
|
||||||
|
}
|
||||||
|
|
||||||
|
async verify(hash: string, plain: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
return await argon2.verify(hash, plain);
|
||||||
|
} catch {
|
||||||
|
// Malformed/foreign hash — treat as verification failure, never throw
|
||||||
|
// into the caller (which would otherwise leak a 500 instead of 401).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needsRehash(algoVersion: number): boolean {
|
||||||
|
return algoVersion !== CURRENT_ALGO_VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
apps/api/src/auth/permission.guard.ts
Normal file
36
apps/api/src/auth/permission.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import type { Request } from "express";
|
||||||
|
import { REQUIRE_PERMISSION_KEY, type RequiredPermission } from "./decorators/require-permission.decorator.js";
|
||||||
|
import { hasPermission } from "./permissions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs after JwtAuthGuard (which populates request.edenUser). Endpoints
|
||||||
|
* without @RequirePermission are left alone — authentication only.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PermissionGuard implements CanActivate {
|
||||||
|
constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const required = this.reflector.get<RequiredPermission | undefined>(
|
||||||
|
REQUIRE_PERMISSION_KEY,
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
if (!required) return true;
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest<Request>();
|
||||||
|
const user = request.edenUser;
|
||||||
|
if (!user) {
|
||||||
|
throw new ForbiddenException("Autenticação necessária");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission(user.permissions, required.resource, required.action)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
`Sem permissão para '${required.action}' em '${required.resource}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
apps/api/src/auth/permissions.ts
Normal file
55
apps/api/src/auth/permissions.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { rolesRepo, type RolePermission } from "@eden/database";
|
||||||
|
|
||||||
|
export const SUPER_ADMIN_ROLE = "super_admin";
|
||||||
|
|
||||||
|
export type ResolvedPermissions =
|
||||||
|
| { superAdmin: true }
|
||||||
|
| { superAdmin: false; grants: RolePermission[] };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* super_admin is never looked up in role_permissions — hardcoded bypass,
|
||||||
|
* mirrored from the legacy system and enforced again at the DB level
|
||||||
|
* (docs/adr/0004-permission-model.md, chk_role_permissions_no_super_admin).
|
||||||
|
*/
|
||||||
|
export async function resolvePermissions(roleKey: string): Promise<ResolvedPermissions> {
|
||||||
|
if (roleKey === SUPER_ADMIN_ROLE) return { superAdmin: true };
|
||||||
|
const grants = await rolesRepo.getRolePermissions(roleKey);
|
||||||
|
return { superAdmin: false, grants };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum scope the actor holds for (resource, action), or
|
||||||
|
* null if they hold none. Scope precedence — broader wins when a role
|
||||||
|
* somehow has more than one row for the same resource+action (shouldn't
|
||||||
|
* happen given the UNIQUE constraint per scope, but a role could have
|
||||||
|
* both e.g. 'own' and 'all' rows intentionally):
|
||||||
|
*/
|
||||||
|
const SCOPE_RANK: Record<RolePermission["scope"], number> = {
|
||||||
|
own: 1,
|
||||||
|
team: 2,
|
||||||
|
reseller: 3,
|
||||||
|
legal_entity: 4,
|
||||||
|
all: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function maxGrantedScope(
|
||||||
|
permissions: ResolvedPermissions,
|
||||||
|
resource: string,
|
||||||
|
action: RolePermission["action"],
|
||||||
|
): RolePermission["scope"] | "all" | null {
|
||||||
|
if (permissions.superAdmin) return "all";
|
||||||
|
const matches = permissions.grants.filter((g) => g.resource === resource && g.action === action);
|
||||||
|
if (matches.length === 0) return null;
|
||||||
|
return matches.reduce<RolePermission["scope"]>(
|
||||||
|
(best, g) => (SCOPE_RANK[g.scope] > SCOPE_RANK[best] ? g.scope : best),
|
||||||
|
matches[0]!.scope,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPermission(
|
||||||
|
permissions: ResolvedPermissions,
|
||||||
|
resource: string,
|
||||||
|
action: RolePermission["action"],
|
||||||
|
): boolean {
|
||||||
|
return maxGrantedScope(permissions, resource, action) !== null;
|
||||||
|
}
|
||||||
45
apps/api/src/auth/token.service.ts
Normal file
45
apps/api/src/auth/token.service.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { randomBytes, createHash } from "node:crypto";
|
||||||
|
import { Inject, Injectable } from "@nestjs/common";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import type { EdenEnv } from "@eden/config";
|
||||||
|
import { EDEN_ENV } from "../config/config.constants.js";
|
||||||
|
|
||||||
|
export interface AccessTokenPayload {
|
||||||
|
sub: string; // user id
|
||||||
|
app: "core" | "reseller" | "subscriber";
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCESS_TOKEN_TTL_SECONDS = 15 * 60; // 15 min — ADR-0003
|
||||||
|
const REFRESH_TOKEN_TTL_DAYS = 30;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TokenService {
|
||||||
|
private readonly secret: string;
|
||||||
|
|
||||||
|
constructor(@Inject(EDEN_ENV) env: EdenEnv) {
|
||||||
|
this.secret = env.JWT_SIGNING_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
signAccessToken(payload: AccessTokenPayload): { token: string; expiresIn: number } {
|
||||||
|
const token = jwt.sign(payload, this.secret, { expiresIn: ACCESS_TOKEN_TTL_SECONDS });
|
||||||
|
return { token, expiresIn: ACCESS_TOKEN_TTL_SECONDS };
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyAccessToken(token: string): AccessTokenPayload {
|
||||||
|
return jwt.verify(token, this.secret) as AccessTokenPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opaque, high-entropy refresh token — never a JWT (ADR-0003). Returns the
|
||||||
|
* plaintext (shown once to the client) and its SHA-256 hash (the only
|
||||||
|
* thing persisted). */
|
||||||
|
generateRefreshToken(): { plaintext: string; hash: string; expiresAt: Date } {
|
||||||
|
const plaintext = randomBytes(32).toString("hex"); // 256 bits
|
||||||
|
const hash = this.hashRefreshToken(plaintext);
|
||||||
|
const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
|
||||||
|
return { plaintext, hash, expiresAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
hashRefreshToken(plaintext: string): string {
|
||||||
|
return createHash("sha256").update(plaintext).digest("hex");
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/api/src/bootstrap/bootstrap.module.ts
Normal file
9
apps/api/src/bootstrap/bootstrap.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthModule } from "../auth/auth.module.js";
|
||||||
|
import { SeedService } from "./seed.service.js";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AuthModule],
|
||||||
|
providers: [SeedService],
|
||||||
|
})
|
||||||
|
export class BootstrapModule {}
|
||||||
50
apps/api/src/bootstrap/seed.service.ts
Normal file
50
apps/api/src/bootstrap/seed.service.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||||
|
import { usersRepo } from "@eden/database";
|
||||||
|
import type { EdenEnv } from "@eden/config";
|
||||||
|
import { EDEN_ENV } from "../config/config.constants.js";
|
||||||
|
import { PasswordService } from "../auth/password.service.js";
|
||||||
|
import { SUPER_ADMIN_ROLE } from "../auth/permissions.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors the legacy's seedAdmin() (eden.md §5.7): runs once at boot,
|
||||||
|
* only acts if `users` is empty, creates the first super_admin from env
|
||||||
|
* vars — never hardcoded, never auto-generated-and-logged. If the env
|
||||||
|
* vars aren't set, boot proceeds without seeding (useful for CI/tests
|
||||||
|
* that don't need a real login) rather than crashing.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class SeedService implements OnModuleInit {
|
||||||
|
private readonly logger = new Logger(SeedService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(EDEN_ENV) private readonly env: EdenEnv,
|
||||||
|
private readonly passwords: PasswordService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
const existing = await usersRepo.countUsers();
|
||||||
|
if (existing > 0) return;
|
||||||
|
|
||||||
|
if (!this.env.EDEN_SUPERADMIN_EMAIL || !this.env.EDEN_SUPERADMIN_PASSWORD) {
|
||||||
|
this.logger.warn(
|
||||||
|
"users está vazia e EDEN_SUPERADMIN_EMAIL/EDEN_SUPERADMIN_PASSWORD não estão definidos — pulando seed do super_admin.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hash, algoVersion } = await this.passwords.hash(this.env.EDEN_SUPERADMIN_PASSWORD);
|
||||||
|
const user = await usersRepo.createUser({
|
||||||
|
email: this.env.EDEN_SUPERADMIN_EMAIL,
|
||||||
|
passwordHash: hash,
|
||||||
|
passwordAlgoVersion: algoVersion,
|
||||||
|
roleKey: SUPER_ADMIN_ROLE,
|
||||||
|
fullName: "Super Administrador",
|
||||||
|
});
|
||||||
|
|
||||||
|
await usersRepo.grantUserApplication(user.id, "core");
|
||||||
|
await usersRepo.grantUserApplication(user.id, "reseller");
|
||||||
|
await usersRepo.grantUserApplication(user.id, "subscriber");
|
||||||
|
|
||||||
|
this.logger.log(`super_admin inicial criado (${user.email}) a partir de EDEN_SUPERADMIN_EMAIL.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/api/src/companies/companies.controller.ts
Normal file
50
apps/api/src/companies/companies.controller.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { CompaniesService } from "./companies.service.js";
|
||||||
|
import { CreateCompanyDto } from "./dto/create-company.dto.js";
|
||||||
|
import { UpdateCompanyDto } from "./dto/update-company.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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read is open to any authenticated user (not gated by a resource
|
||||||
|
* permission) — company data isn't sensitive and is needed to render
|
||||||
|
* contracts/documents from screens any seller/backoffice user can reach,
|
||||||
|
* mirroring the legacy (eden.md §1, §6). Write requires companies:manage.
|
||||||
|
*/
|
||||||
|
@ApiTags("companies")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, PermissionGuard)
|
||||||
|
@Controller("v1/companies")
|
||||||
|
export class CompaniesController {
|
||||||
|
constructor(private readonly companies: CompaniesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
list(@Query("includeInactive") includeInactive?: string) {
|
||||||
|
return this.companies.list(includeInactive === "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
get(@Param("id") id: string) {
|
||||||
|
return this.companies.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@RequirePermission("companies", "manage")
|
||||||
|
create(@Body() dto: CreateCompanyDto, @CurrentUser() user: EdenAuthenticatedUser) {
|
||||||
|
return this.companies.create(dto, user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@RequirePermission("companies", "manage")
|
||||||
|
update(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() dto: UpdateCompanyDto,
|
||||||
|
@CurrentUser() user: EdenAuthenticatedUser,
|
||||||
|
) {
|
||||||
|
return this.companies.update(id, dto, user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
apps/api/src/companies/companies.module.ts
Normal file
11
apps/api/src/companies/companies.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { AuthModule } from "../auth/auth.module.js";
|
||||||
|
import { CompaniesController } from "./companies.controller.js";
|
||||||
|
import { CompaniesService } from "./companies.service.js";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [AuthModule],
|
||||||
|
controllers: [CompaniesController],
|
||||||
|
providers: [CompaniesService],
|
||||||
|
})
|
||||||
|
export class CompaniesModule {}
|
||||||
102
apps/api/src/companies/companies.service.ts
Normal file
102
apps/api/src/companies/companies.service.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { companiesRepo, auditRepo, type Company } from "@eden/database";
|
||||||
|
import { onlyDigits, isValidCNPJ } from "@eden/domain-shared";
|
||||||
|
import type { CreateCompanyDto } from "./dto/create-company.dto.js";
|
||||||
|
import type { UpdateCompanyDto } from "./dto/update-company.dto.js";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CompaniesService {
|
||||||
|
list(includeInactive: boolean): Promise<Company[]> {
|
||||||
|
return companiesRepo.listCompanies(includeInactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<Company> {
|
||||||
|
const company = await companiesRepo.getCompanyById(id);
|
||||||
|
if (!company) throw new NotFoundException("Empresa não encontrada");
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(dto: CreateCompanyDto, actorId: string): Promise<Company> {
|
||||||
|
this.assertValidCnpjIfPresent(dto.cnpj);
|
||||||
|
const company = await companiesRepo.createCompany({
|
||||||
|
companyName: dto.companyName,
|
||||||
|
tradeName: dto.tradeName,
|
||||||
|
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
|
||||||
|
stateRegistration: dto.stateRegistration,
|
||||||
|
municipalRegistration: dto.municipalRegistration,
|
||||||
|
addressZip: dto.addressZip,
|
||||||
|
addressStreet: dto.addressStreet,
|
||||||
|
addressNumber: dto.addressNumber,
|
||||||
|
addressComplement: dto.addressComplement,
|
||||||
|
addressNeighborhood: dto.addressNeighborhood,
|
||||||
|
addressCity: dto.addressCity,
|
||||||
|
addressState: dto.addressState,
|
||||||
|
addressCountry: dto.addressCountry,
|
||||||
|
phone: dto.phone,
|
||||||
|
email: dto.email,
|
||||||
|
website: dto.website,
|
||||||
|
legalRepName: dto.legalRepName,
|
||||||
|
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
|
||||||
|
legalRepRole: dto.legalRepRole,
|
||||||
|
legalRepEmail: dto.legalRepEmail,
|
||||||
|
createdBy: actorId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "user",
|
||||||
|
actorId,
|
||||||
|
action: "company.created",
|
||||||
|
resourceType: "company",
|
||||||
|
resourceId: company.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateCompanyDto, actorId: string): Promise<Company> {
|
||||||
|
await this.get(id); // 404 if missing
|
||||||
|
this.assertValidCnpjIfPresent(dto.cnpj);
|
||||||
|
|
||||||
|
const company = await companiesRepo.updateCompany(id, {
|
||||||
|
companyName: dto.companyName,
|
||||||
|
tradeName: dto.tradeName,
|
||||||
|
cnpj: dto.cnpj ? onlyDigits(dto.cnpj) : undefined,
|
||||||
|
stateRegistration: dto.stateRegistration,
|
||||||
|
municipalRegistration: dto.municipalRegistration,
|
||||||
|
addressZip: dto.addressZip,
|
||||||
|
addressStreet: dto.addressStreet,
|
||||||
|
addressNumber: dto.addressNumber,
|
||||||
|
addressComplement: dto.addressComplement,
|
||||||
|
addressNeighborhood: dto.addressNeighborhood,
|
||||||
|
addressCity: dto.addressCity,
|
||||||
|
addressState: dto.addressState,
|
||||||
|
addressCountry: dto.addressCountry,
|
||||||
|
phone: dto.phone,
|
||||||
|
email: dto.email,
|
||||||
|
website: dto.website,
|
||||||
|
legalRepName: dto.legalRepName,
|
||||||
|
legalRepCpf: dto.legalRepCpf ? onlyDigits(dto.legalRepCpf) : undefined,
|
||||||
|
legalRepRole: dto.legalRepRole,
|
||||||
|
legalRepEmail: dto.legalRepEmail,
|
||||||
|
isActive: dto.isActive,
|
||||||
|
});
|
||||||
|
if (!company) throw new NotFoundException("Empresa não encontrada");
|
||||||
|
|
||||||
|
await auditRepo.appendAuditEvent({
|
||||||
|
actorType: "user",
|
||||||
|
actorId,
|
||||||
|
action: "company.updated",
|
||||||
|
resourceType: "company",
|
||||||
|
resourceId: id,
|
||||||
|
metadata: { fields: Object.keys(dto) },
|
||||||
|
});
|
||||||
|
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertValidCnpjIfPresent(cnpj: string | undefined): void {
|
||||||
|
if (cnpj && !isValidCNPJ(cnpj)) {
|
||||||
|
throw new BadRequestException("CNPJ inválido");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
apps/api/src/companies/dto/create-company.dto.ts
Normal file
27
apps/api/src/companies/dto/create-company.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { IsEmail, IsOptional, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateCompanyDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
companyName!: string;
|
||||||
|
|
||||||
|
@IsOptional() @IsString() tradeName?: string;
|
||||||
|
@IsOptional() @IsString() cnpj?: string;
|
||||||
|
@IsOptional() @IsString() stateRegistration?: string;
|
||||||
|
@IsOptional() @IsString() municipalRegistration?: string;
|
||||||
|
@IsOptional() @IsString() addressZip?: string;
|
||||||
|
@IsOptional() @IsString() addressStreet?: string;
|
||||||
|
@IsOptional() @IsString() addressNumber?: string;
|
||||||
|
@IsOptional() @IsString() addressComplement?: string;
|
||||||
|
@IsOptional() @IsString() addressNeighborhood?: string;
|
||||||
|
@IsOptional() @IsString() addressCity?: string;
|
||||||
|
@IsOptional() @IsString() addressState?: string;
|
||||||
|
@IsOptional() @IsString() addressCountry?: string;
|
||||||
|
@IsOptional() @IsString() phone?: string;
|
||||||
|
@IsOptional() @IsEmail() email?: string;
|
||||||
|
@IsOptional() @IsString() website?: string;
|
||||||
|
@IsOptional() @IsString() legalRepName?: string;
|
||||||
|
@IsOptional() @IsString() legalRepCpf?: string;
|
||||||
|
@IsOptional() @IsString() legalRepRole?: string;
|
||||||
|
@IsOptional() @IsEmail() legalRepEmail?: string;
|
||||||
|
}
|
||||||
9
apps/api/src/companies/dto/update-company.dto.ts
Normal file
9
apps/api/src/companies/dto/update-company.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { PartialType } from "@nestjs/swagger";
|
||||||
|
import { IsOptional, IsBoolean } from "class-validator";
|
||||||
|
import { CreateCompanyDto } from "./create-company.dto.js";
|
||||||
|
|
||||||
|
export class UpdateCompanyDto extends PartialType(CreateCompanyDto) {
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
1
apps/api/src/config/config.constants.ts
Normal file
1
apps/api/src/config/config.constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const EDEN_ENV = Symbol("EDEN_ENV");
|
||||||
19
apps/api/src/config/config.module.ts
Normal file
19
apps/api/src/config/config.module.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { loadEnv } from "@eden/config";
|
||||||
|
import { EDEN_ENV } from "./config.constants.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fails fast at boot (via loadEnv) if a required secret/config var is
|
||||||
|
* missing — see packages/config/src/env-schema.ts and eden.md §5.6.
|
||||||
|
*/
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: EDEN_ENV,
|
||||||
|
useFactory: () => loadEnv(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [EDEN_ENV],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
||||||
@@ -1,13 +1,38 @@
|
|||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
|
import { ValidationPipe } from "@nestjs/common";
|
||||||
|
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
app.setGlobalPrefix("api", { exclude: ["health/live", "health/ready"] });
|
||||||
|
|
||||||
|
// Server-side validation is the authority — never trust that a client
|
||||||
|
// already validated (Master Prompt §12). Strips unknown fields instead
|
||||||
|
// of accepting them silently (mass-assignment protection).
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const openApiConfig = new DocumentBuilder()
|
||||||
|
.setTitle("EDEN API")
|
||||||
|
.setDescription("API principal do EDEN (Core, Parceiros, Assinante)")
|
||||||
|
.setVersion("0.1.0")
|
||||||
|
.addBearerAuth()
|
||||||
|
.build();
|
||||||
|
const document = SwaggerModule.createDocument(app, openApiConfig);
|
||||||
|
SwaggerModule.setup("api/docs", app, document);
|
||||||
|
|
||||||
const port = Number(process.env.EDEN_API_PORT ?? 8080);
|
const port = Number(process.env.EDEN_API_PORT ?? 8080);
|
||||||
await app.listen(port, "0.0.0.0");
|
await app.listen(port, "0.0.0.0");
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`[eden-api] listening on :${port}`);
|
console.log(`[eden-api] listening on :${port} — docs em /api/docs`);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ services:
|
|||||||
JWT_SIGNING_SECRET: ${JWT_SIGNING_SECRET}
|
JWT_SIGNING_SECRET: ${JWT_SIGNING_SECRET}
|
||||||
SIGNATURE_OTP_SECRET: ${SIGNATURE_OTP_SECRET}
|
SIGNATURE_OTP_SECRET: ${SIGNATURE_OTP_SECRET}
|
||||||
EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY}
|
EDEN_FIELD_ENCRYPTION_KEY: ${EDEN_FIELD_ENCRYPTION_KEY}
|
||||||
|
EDEN_SUPERADMIN_EMAIL: ${EDEN_SUPERADMIN_EMAIL}
|
||||||
|
EDEN_SUPERADMIN_PASSWORD: ${EDEN_SUPERADMIN_PASSWORD}
|
||||||
SMTP_HOST: ${SMTP_HOST}
|
SMTP_HOST: ${SMTP_HOST}
|
||||||
SMTP_PORT: ${SMTP_PORT}
|
SMTP_PORT: ${SMTP_PORT}
|
||||||
SMTP_USER: ${SMTP_USER}
|
SMTP_USER: ${SMTP_USER}
|
||||||
|
|||||||
@@ -74,10 +74,20 @@ Bugs de bootstrap corrigidos nesta sessão (documentados aqui para não se repet
|
|||||||
3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`.
|
3. `node-pg-migrate --envPath` não carregava o `.env` corretamente nesta versão — trocado por `bash -c 'source .env; ...'` nos scripts `migrate:up`/`migrate:down`.
|
||||||
4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`.
|
4. Healthcheck dos 3 frontends usava `wget http://localhost/...`, que resolve para `::1` (IPv6) dentro do container — nginx só escuta IPv4; trocado para `127.0.0.1`.
|
||||||
|
|
||||||
|
### Auth, permissões, Companies e OpenAPI — implementados e testados ao vivo
|
||||||
|
|
||||||
|
- **Auth** (`apps/api/src/auth/`): Argon2id (parâmetros versionados, ADR-0005), JWT de acesso de 15 min, refresh token opaco (256 bits) com rotação e **detecção de replay** (reuso de token já rotacionado revoga toda a cadeia de sessões do usuário). Endpoints `POST /api/v1/auth/{login,refresh,logout}` e `GET /api/v1/auth/me`.
|
||||||
|
- **Permissões** (`apps/api/src/auth/permissions.ts`, `permission.guard.ts`): resource+action+scope (ADR-0004), `super_admin` com bypass hardcoded (nunca consulta `role_permissions`, espelhando a trava também existente no banco). Guard resolve permissões do banco a cada request (nunca cacheado no JWT) — mudança de papel tem efeito imediato, mesma filosofia do legado.
|
||||||
|
- **Companies** (`apps/api/src/companies/` + migration `1788437478189_companies`): CRUD com leitura aberta a qualquer autenticado e escrita restrita a `companies:manage`; validação de CNPJ (dígito verificador, `packages/domain-shared`) testada com CNPJ válido/inválido conhecidos.
|
||||||
|
- **Seed automático**: `SeedService` cria o primeiro `super_admin` a partir de `EDEN_SUPERADMIN_EMAIL`/`PASSWORD` só quando `users` está vazia — testado (log `super_admin inicial criado`).
|
||||||
|
- **OpenAPI/Swagger**: `/api/docs` (UI) e `/api/docs-json`, cobrindo todos os endpoints atuais.
|
||||||
|
- **Validação/segurança de request**: `ValidationPipe` global com `whitelist`+`forbidNonWhitelisted` (proteção contra mass-assignment) — testado (`id`/`isActive` extras num POST de company são rejeitados com 400).
|
||||||
|
|
||||||
|
Testes manuais realizados contra a stack real (não só typecheck): login com senha errada → 401 genérico; refresh rotaciona e o token antigo some; reuso do token antigo → 401 + toda a cadeia revogada; logout revoga e é idempotente; audit log com a sequência completa de eventos (`auth.login`, `auth.refresh`, `auth.refresh_replay_detected`, `company.created`).
|
||||||
|
|
||||||
### Pendências da Fase 1 (não iniciadas)
|
### Pendências da Fase 1 (não iniciadas)
|
||||||
- Módulo de autenticação real (Argon2id, JWT curto + refresh rotativo, MFA) — ADR-0003 desenhada, implementação ainda não começou.
|
- MFA/TOTP (schema e endpoint ainda não implementados — `users.mfa_*` já existe no schema).
|
||||||
- Companies/Legal Entities (Organization).
|
|
||||||
- Adapters S3/SMTP.
|
- Adapters S3/SMTP.
|
||||||
- OpenAPI 3.1.
|
|
||||||
- Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder).
|
- Extração do Design System a partir do tema DreamsERP (`packages/ui` ainda é placeholder).
|
||||||
- Nenhum commit git feito ainda — aguardando solicitação explícita do operador.
|
- CRUD de usuários/papéis (convite, reset de senha, gestão de `role_permissions`) — hoje só existe o super_admin seedado; não há endpoint para criar outros usuários ainda.
|
||||||
|
- Nota de dado de teste: a empresa "Handix Telecom LTDA" criada durante os testes usa um CNPJ de exemplo publicamente conhecido (`11.222.333/0001-81`), não o CNPJ real da Handix — substituir/remover antes de qualquer ambiente que não seja dev local.
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"lint": "echo 'no linter configured yet'"
|
"lint": "echo 'no linter configured yet'"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
packages/config/src/env-schema.ts
Normal file
50
packages/config/src/env-schema.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail fast on boot if a required secret/config value is missing — same
|
||||||
|
* philosophy as the legacy's JWT_SECRET/SEED_ADMIN_* checks (eden.md §5.6).
|
||||||
|
* Never fall back to an insecure default for a secret.
|
||||||
|
*/
|
||||||
|
export const envSchema = z.object({
|
||||||
|
DATABASE_URL: z.string().min(1, "DATABASE_URL é obrigatório"),
|
||||||
|
EDEN_API_PORT: z.coerce.number().int().positive().default(8080),
|
||||||
|
|
||||||
|
JWT_SIGNING_SECRET: z.string().min(32, "JWT_SIGNING_SECRET precisa ter ao menos 32 caracteres"),
|
||||||
|
SIGNATURE_OTP_SECRET: z.string().min(32).optional(),
|
||||||
|
EDEN_FIELD_ENCRYPTION_KEY: z
|
||||||
|
.string()
|
||||||
|
.min(32, "EDEN_FIELD_ENCRYPTION_KEY precisa ter ao menos 32 caracteres")
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
EDEN_SUPERADMIN_EMAIL: z.string().email().optional(),
|
||||||
|
EDEN_SUPERADMIN_PASSWORD: z.string().min(8).optional(),
|
||||||
|
|
||||||
|
SMTP_HOST: z.string().optional(),
|
||||||
|
SMTP_PORT: z.coerce.number().int().positive().default(465),
|
||||||
|
SMTP_USER: z.string().optional(),
|
||||||
|
SMTP_PASS: z.string().optional(),
|
||||||
|
SMTP_FROM: z.string().optional(),
|
||||||
|
|
||||||
|
S3_ENDPOINT: z.string().optional(),
|
||||||
|
S3_REGION: z.string().default("us-east-1"),
|
||||||
|
S3_FORCE_PATH_STYLE: z
|
||||||
|
.string()
|
||||||
|
.default("true")
|
||||||
|
.transform((v) => v === "true"),
|
||||||
|
S3_ACCESS_KEY_ID: z.string().optional(),
|
||||||
|
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
||||||
|
S3_BUCKET: z.string().optional(),
|
||||||
|
|
||||||
|
PUBLIC_BASE_URL: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EdenEnv = z.infer<typeof envSchema>;
|
||||||
|
|
||||||
|
export function loadEnv(source: NodeJS.ProcessEnv = process.env): EdenEnv {
|
||||||
|
const result = envSchema.safeParse(source);
|
||||||
|
if (!result.success) {
|
||||||
|
const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
|
||||||
|
throw new Error(`Configuração de ambiente inválida:\n${issues}`);
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
@@ -1,6 +1 @@
|
|||||||
/**
|
export { envSchema, loadEnv, type EdenEnv } from "./env-schema.js";
|
||||||
* Shared env-var schema/validation (fail fast on boot if a required var is
|
|
||||||
* missing — same philosophy as the legacy's JWT_SECRET/SEED_ADMIN_* checks,
|
|
||||||
* eden.md §5.6). Placeholder until apps/api's config module needs it.
|
|
||||||
*/
|
|
||||||
export const CONFIG_PACKAGE_VERSION = "0.1.0";
|
|
||||||
|
|||||||
54
packages/database/migrations/1788437478189_companies.cjs
Normal file
54
packages/database/migrations/1788437478189_companies.cjs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Organization bounded context, first slice: companies (legal entities
|
||||||
|
* that issue contracts/documents) — Master Prompt §6.1. Shape ported
|
||||||
|
* faithfully from the legacy (eden.md §1.4), since this is validated,
|
||||||
|
* production-tested data. Warehouses/branches/cost centers land in a
|
||||||
|
* later migration once Inventory/Finance need them — Master Prompt §6.1
|
||||||
|
* explicitly warns against leaving `companies` isolated forever, but
|
||||||
|
* that linkage is real work for those later phases, not invented now.
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.shorthands = undefined;
|
||||||
|
|
||||||
|
exports.up = (pgm) => {
|
||||||
|
pgm.sql(`
|
||||||
|
CREATE TABLE companies (
|
||||||
|
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),
|
||||||
|
company_name TEXT NOT NULL,
|
||||||
|
trade_name TEXT,
|
||||||
|
cnpj TEXT,
|
||||||
|
state_registration TEXT,
|
||||||
|
municipal_registration TEXT,
|
||||||
|
address_zip TEXT,
|
||||||
|
address_street TEXT,
|
||||||
|
address_number TEXT,
|
||||||
|
address_complement TEXT,
|
||||||
|
address_neighborhood TEXT,
|
||||||
|
address_city TEXT,
|
||||||
|
address_state TEXT,
|
||||||
|
address_country TEXT NOT NULL DEFAULT 'Brasil',
|
||||||
|
phone TEXT,
|
||||||
|
email TEXT,
|
||||||
|
website TEXT,
|
||||||
|
legal_rep_name TEXT,
|
||||||
|
legal_rep_cpf TEXT,
|
||||||
|
legal_rep_role TEXT,
|
||||||
|
legal_rep_email TEXT,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT true
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE companies IS
|
||||||
|
'Empresa(s) emissora(s) de contrato/documento — eden.md §1.4. Leitura aberta a qualquer autenticado (não sensível), escrita restrita à permissão companies:manage.';
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_companies_updated_at BEFORE UPDATE ON companies
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
CREATE INDEX idx_companies_active ON companies(is_active);
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (pgm) => {
|
||||||
|
pgm.sql(`DROP TABLE IF EXISTS companies;`);
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
"node-pg-migrate": "^7.9.1",
|
"node-pg-migrate": "^7.9.1",
|
||||||
"typescript": "^5.7.3"
|
"typescript": "^5.7.3"
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
export { query, withTransaction, closePool } from "./pool.js";
|
export { query, withTransaction, closePool } from "./pool.js";
|
||||||
|
export * as rolesRepo from "./repositories/roles.js";
|
||||||
|
export * as usersRepo from "./repositories/users.js";
|
||||||
|
export * as sessionsRepo from "./repositories/sessions.js";
|
||||||
|
export * as auditRepo from "./repositories/audit.js";
|
||||||
|
export * as companiesRepo from "./repositories/companies.js";
|
||||||
|
export type * from "./repositories/types.js";
|
||||||
|
export type { Company } from "./repositories/companies.js";
|
||||||
|
|||||||
65
packages/database/src/repositories/audit.ts
Normal file
65
packages/database/src/repositories/audit.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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<string, unknown>;
|
||||||
|
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<string, unknown>;
|
||||||
|
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. 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, which is fine (nothing was recorded), but a *committed*
|
||||||
|
* event must never be skipped when computing previous_event_hash for the
|
||||||
|
* next one. See .claude/skills/eden-security/references/audit.md.
|
||||||
|
*/
|
||||||
|
export async function appendAuditEvent(input: AuditEventInput): Promise<void> {
|
||||||
|
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,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
163
packages/database/src/repositories/companies.ts
Normal file
163
packages/database/src/repositories/companies.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { query } from "../pool.js";
|
||||||
|
|
||||||
|
export interface Company {
|
||||||
|
id: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string | null;
|
||||||
|
company_name: string;
|
||||||
|
trade_name: string | null;
|
||||||
|
cnpj: string | null;
|
||||||
|
state_registration: string | null;
|
||||||
|
municipal_registration: string | null;
|
||||||
|
address_zip: string | null;
|
||||||
|
address_street: string | null;
|
||||||
|
address_number: string | null;
|
||||||
|
address_complement: string | null;
|
||||||
|
address_neighborhood: string | null;
|
||||||
|
address_city: string | null;
|
||||||
|
address_state: string | null;
|
||||||
|
address_country: string;
|
||||||
|
phone: string | null;
|
||||||
|
email: string | null;
|
||||||
|
website: string | null;
|
||||||
|
legal_rep_name: string | null;
|
||||||
|
legal_rep_cpf: string | null;
|
||||||
|
legal_rep_role: string | null;
|
||||||
|
legal_rep_email: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCompanies(includeInactive = false): Promise<Company[]> {
|
||||||
|
const { rows } = await query<Company>(
|
||||||
|
includeInactive
|
||||||
|
? "SELECT * FROM companies ORDER BY created_at ASC"
|
||||||
|
: "SELECT * FROM companies WHERE is_active = true ORDER BY created_at ASC",
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCompanyById(id: string): Promise<Company | null> {
|
||||||
|
const { rows } = await query<Company>("SELECT * FROM companies WHERE id = $1", [id]);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateCompanyInput {
|
||||||
|
companyName: string;
|
||||||
|
tradeName?: string | null;
|
||||||
|
cnpj?: string | null;
|
||||||
|
stateRegistration?: string | null;
|
||||||
|
municipalRegistration?: string | null;
|
||||||
|
addressZip?: string | null;
|
||||||
|
addressStreet?: string | null;
|
||||||
|
addressNumber?: string | null;
|
||||||
|
addressComplement?: string | null;
|
||||||
|
addressNeighborhood?: string | null;
|
||||||
|
addressCity?: string | null;
|
||||||
|
addressState?: string | null;
|
||||||
|
addressCountry?: string;
|
||||||
|
phone?: string | null;
|
||||||
|
email?: string | null;
|
||||||
|
website?: string | null;
|
||||||
|
legalRepName?: string | null;
|
||||||
|
legalRepCpf?: string | null;
|
||||||
|
legalRepRole?: string | null;
|
||||||
|
legalRepEmail?: string | null;
|
||||||
|
createdBy?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCompany(input: CreateCompanyInput): Promise<Company> {
|
||||||
|
const { rows } = await query<Company>(
|
||||||
|
`INSERT INTO companies (
|
||||||
|
company_name, trade_name, cnpj, state_registration, municipal_registration,
|
||||||
|
address_zip, address_street, address_number, address_complement, address_neighborhood,
|
||||||
|
address_city, address_state, address_country, phone, email, website,
|
||||||
|
legal_rep_name, legal_rep_cpf, legal_rep_role, legal_rep_email, created_by
|
||||||
|
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,COALESCE($13,'Brasil'),$14,$15,$16,$17,$18,$19,$20,$21)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
input.companyName,
|
||||||
|
input.tradeName ?? null,
|
||||||
|
input.cnpj ?? null,
|
||||||
|
input.stateRegistration ?? null,
|
||||||
|
input.municipalRegistration ?? null,
|
||||||
|
input.addressZip ?? null,
|
||||||
|
input.addressStreet ?? null,
|
||||||
|
input.addressNumber ?? null,
|
||||||
|
input.addressComplement ?? null,
|
||||||
|
input.addressNeighborhood ?? null,
|
||||||
|
input.addressCity ?? null,
|
||||||
|
input.addressState ?? null,
|
||||||
|
input.addressCountry ?? null,
|
||||||
|
input.phone ?? null,
|
||||||
|
input.email ?? null,
|
||||||
|
input.website ?? null,
|
||||||
|
input.legalRepName ?? null,
|
||||||
|
input.legalRepCpf ?? null,
|
||||||
|
input.legalRepRole ?? null,
|
||||||
|
input.legalRepEmail ?? null,
|
||||||
|
input.createdBy ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const company = rows[0];
|
||||||
|
if (!company) throw new Error("failed to create company");
|
||||||
|
return company;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateCompanyInput = Partial<Omit<CreateCompanyInput, "createdBy">> & {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function updateCompany(id: string, input: UpdateCompanyInput): Promise<Company | null> {
|
||||||
|
const { rows } = await query<Company>(
|
||||||
|
`UPDATE companies SET
|
||||||
|
company_name = COALESCE($2, company_name),
|
||||||
|
trade_name = COALESCE($3, trade_name),
|
||||||
|
cnpj = COALESCE($4, cnpj),
|
||||||
|
state_registration = COALESCE($5, state_registration),
|
||||||
|
municipal_registration = COALESCE($6, municipal_registration),
|
||||||
|
address_zip = COALESCE($7, address_zip),
|
||||||
|
address_street = COALESCE($8, address_street),
|
||||||
|
address_number = COALESCE($9, address_number),
|
||||||
|
address_complement = COALESCE($10, address_complement),
|
||||||
|
address_neighborhood = COALESCE($11, address_neighborhood),
|
||||||
|
address_city = COALESCE($12, address_city),
|
||||||
|
address_state = COALESCE($13, address_state),
|
||||||
|
address_country = COALESCE($14, address_country),
|
||||||
|
phone = COALESCE($15, phone),
|
||||||
|
email = COALESCE($16, email),
|
||||||
|
website = COALESCE($17, website),
|
||||||
|
legal_rep_name = COALESCE($18, legal_rep_name),
|
||||||
|
legal_rep_cpf = COALESCE($19, legal_rep_cpf),
|
||||||
|
legal_rep_role = COALESCE($20, legal_rep_role),
|
||||||
|
legal_rep_email = COALESCE($21, legal_rep_email),
|
||||||
|
is_active = COALESCE($22, is_active)
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
id,
|
||||||
|
input.companyName ?? null,
|
||||||
|
input.tradeName ?? null,
|
||||||
|
input.cnpj ?? null,
|
||||||
|
input.stateRegistration ?? null,
|
||||||
|
input.municipalRegistration ?? null,
|
||||||
|
input.addressZip ?? null,
|
||||||
|
input.addressStreet ?? null,
|
||||||
|
input.addressNumber ?? null,
|
||||||
|
input.addressComplement ?? null,
|
||||||
|
input.addressNeighborhood ?? null,
|
||||||
|
input.addressCity ?? null,
|
||||||
|
input.addressState ?? null,
|
||||||
|
input.addressCountry ?? null,
|
||||||
|
input.phone ?? null,
|
||||||
|
input.email ?? null,
|
||||||
|
input.website ?? null,
|
||||||
|
input.legalRepName ?? null,
|
||||||
|
input.legalRepCpf ?? null,
|
||||||
|
input.legalRepRole ?? null,
|
||||||
|
input.legalRepEmail ?? null,
|
||||||
|
input.isActive ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
20
packages/database/src/repositories/roles.ts
Normal file
20
packages/database/src/repositories/roles.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { query } from "../pool.js";
|
||||||
|
import type { Role, RolePermission } from "./types.js";
|
||||||
|
|
||||||
|
export async function getRoleByKey(key: string): Promise<Role | null> {
|
||||||
|
const { rows } = await query<Role>("SELECT * FROM roles WHERE key = $1", [key]);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRoles(): Promise<Role[]> {
|
||||||
|
const { rows } = await query<Role>("SELECT * FROM roles ORDER BY weight ASC");
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRolePermissions(roleKey: string): Promise<RolePermission[]> {
|
||||||
|
const { rows } = await query<RolePermission>(
|
||||||
|
"SELECT * FROM role_permissions WHERE role_key = $1",
|
||||||
|
[roleKey],
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
84
packages/database/src/repositories/sessions.ts
Normal file
84
packages/database/src/repositories/sessions.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { query } from "../pool.js";
|
||||||
|
import type { Session } from "./types.js";
|
||||||
|
|
||||||
|
export interface CreateSessionInput {
|
||||||
|
userId: string;
|
||||||
|
applicationKey: "core" | "reseller" | "subscriber";
|
||||||
|
refreshTokenHash: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
ipAddress?: string | null;
|
||||||
|
userAgent?: string | null;
|
||||||
|
replacesSessionId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(input: CreateSessionInput): Promise<Session> {
|
||||||
|
const { rows } = await query<Session>(
|
||||||
|
`INSERT INTO sessions (user_id, application_key, refresh_token_hash, expires_at, ip_address, user_agent)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
input.userId,
|
||||||
|
input.applicationKey,
|
||||||
|
input.refreshTokenHash,
|
||||||
|
input.expiresAt,
|
||||||
|
input.ipAddress ?? null,
|
||||||
|
input.userAgent ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const session = rows[0];
|
||||||
|
if (!session) throw new Error("failed to create session");
|
||||||
|
|
||||||
|
if (input.replacesSessionId) {
|
||||||
|
await query("UPDATE sessions SET replaced_by_session_id = $1, revoked_at = now() WHERE id = $2", [
|
||||||
|
session.id,
|
||||||
|
input.replacesSessionId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findActiveSessionByRefreshTokenHash(hash: string): Promise<Session | null> {
|
||||||
|
const { rows } = await query<Session>(
|
||||||
|
`SELECT * FROM sessions
|
||||||
|
WHERE refresh_token_hash = $1 AND revoked_at IS NULL AND expires_at > now()`,
|
||||||
|
[hash],
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a session by refresh token hash regardless of revoked/expired state —
|
||||||
|
* used for replay detection: a rotated (already-revoked) token being
|
||||||
|
* reused is a strong signal of a stolen refresh token (ADR-0003).
|
||||||
|
*/
|
||||||
|
export async function findSessionByRefreshTokenHashAnyState(hash: string): Promise<Session | null> {
|
||||||
|
const { rows } = await query<Session>("SELECT * FROM sessions WHERE refresh_token_hash = $1", [
|
||||||
|
hash,
|
||||||
|
]);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function touchSession(id: string): Promise<void> {
|
||||||
|
await query("UPDATE sessions SET last_used_at = now() WHERE id = $1", [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function revokeSession(id: string): Promise<void> {
|
||||||
|
await query("UPDATE sessions SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL", [id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Revokes every active session of a user — "encerrar todas as sessões" (Master Prompt §5.3). */
|
||||||
|
export async function revokeAllUserSessions(userId: string): Promise<void> {
|
||||||
|
await query("UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL", [
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listActiveSessionsForUser(userId: string): Promise<Session[]> {
|
||||||
|
const { rows } = await query<Session>(
|
||||||
|
`SELECT * FROM sessions WHERE user_id = $1 AND revoked_at IS NULL AND expires_at > now()
|
||||||
|
ORDER BY created_at DESC`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
53
packages/database/src/repositories/types.ts
Normal file
53
packages/database/src/repositories/types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export interface Role {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
is_system: boolean;
|
||||||
|
weight: number;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RolePermission {
|
||||||
|
id: string;
|
||||||
|
role_key: string;
|
||||||
|
resource: string;
|
||||||
|
action: "view" | "create" | "edit" | "delete" | "approve" | "export" | "manage";
|
||||||
|
scope: "own" | "team" | "reseller" | "legal_entity" | "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
created_by: string | null;
|
||||||
|
full_name: string | null;
|
||||||
|
email: string;
|
||||||
|
password_hash: string | null;
|
||||||
|
password_algo_version: number;
|
||||||
|
role_key: string;
|
||||||
|
is_active: boolean;
|
||||||
|
reset_token_hash: string | null;
|
||||||
|
reset_token_expires_at: Date | null;
|
||||||
|
mfa_enabled: boolean;
|
||||||
|
mfa_secret_encrypted: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
application_key: "core" | "reseller" | "subscriber";
|
||||||
|
refresh_token_hash: string;
|
||||||
|
created_at: Date;
|
||||||
|
last_used_at: Date | null;
|
||||||
|
expires_at: Date;
|
||||||
|
revoked_at: Date | null;
|
||||||
|
replaced_by_session_id: string | null;
|
||||||
|
ip_address: string | null;
|
||||||
|
user_agent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Application {
|
||||||
|
key: "core" | "reseller" | "subscriber";
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
80
packages/database/src/repositories/users.ts
Normal file
80
packages/database/src/repositories/users.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { query } from "../pool.js";
|
||||||
|
import type { User } from "./types.js";
|
||||||
|
|
||||||
|
export async function countUsers(): Promise<number> {
|
||||||
|
const { rows } = await query<{ count: string }>("SELECT count(*) FROM users");
|
||||||
|
return Number(rows[0]?.count ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findUserByEmail(email: string): Promise<User | null> {
|
||||||
|
const { rows } = await query<User>(
|
||||||
|
"SELECT * FROM users WHERE lower(email) = lower($1) AND is_active = true",
|
||||||
|
[email],
|
||||||
|
);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function findUserById(id: string): Promise<User | null> {
|
||||||
|
const { rows } = await query<User>("SELECT * FROM users WHERE id = $1", [id]);
|
||||||
|
return rows[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateUserInput {
|
||||||
|
email: string;
|
||||||
|
passwordHash: string;
|
||||||
|
passwordAlgoVersion: number;
|
||||||
|
roleKey: string;
|
||||||
|
fullName?: string;
|
||||||
|
createdBy?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(input: CreateUserInput): Promise<User> {
|
||||||
|
const { rows } = await query<User>(
|
||||||
|
`INSERT INTO users (email, password_hash, password_algo_version, role_key, full_name, created_by)
|
||||||
|
VALUES (lower($1), $2, $3, $4, $5, $6)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
input.email,
|
||||||
|
input.passwordHash,
|
||||||
|
input.passwordAlgoVersion,
|
||||||
|
input.roleKey,
|
||||||
|
input.fullName ?? null,
|
||||||
|
input.createdBy ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const user = rows[0];
|
||||||
|
if (!user) throw new Error("failed to create user");
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUserPassword(
|
||||||
|
userId: string,
|
||||||
|
passwordHash: string,
|
||||||
|
passwordAlgoVersion: number,
|
||||||
|
): Promise<void> {
|
||||||
|
await query("UPDATE users SET password_hash = $1, password_algo_version = $2 WHERE id = $3", [
|
||||||
|
passwordHash,
|
||||||
|
passwordAlgoVersion,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function grantUserApplication(
|
||||||
|
userId: string,
|
||||||
|
applicationKey: "core" | "reseller" | "subscriber",
|
||||||
|
): Promise<void> {
|
||||||
|
await query(
|
||||||
|
`INSERT INTO user_applications (user_id, application_key)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, application_key) DO NOTHING`,
|
||||||
|
[userId, applicationKey],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserApplications(userId: string): Promise<string[]> {
|
||||||
|
const { rows } = await query<{ application_key: string }>(
|
||||||
|
"SELECT application_key FROM user_applications WHERE user_id = $1",
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
return rows.map((r) => r.application_key);
|
||||||
|
}
|
||||||
@@ -1,7 +1,2 @@
|
|||||||
/**
|
|
||||||
* Domain types/constants shared across bounded contexts (e.g. the
|
|
||||||
* resource/action/scope permission vocabulary from
|
|
||||||
* docs/adr/0004-permission-model.md). Placeholder until a second
|
|
||||||
* bounded context needs to share a type with the first.
|
|
||||||
*/
|
|
||||||
export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0";
|
export const DOMAIN_SHARED_PACKAGE_VERSION = "0.1.0";
|
||||||
|
export { onlyDigits, isValidCPF, isValidCNPJ } from "./validators/br-documents.js";
|
||||||
|
|||||||
49
packages/domain-shared/src/validators/br-documents.ts
Normal file
49
packages/domain-shared/src/validators/br-documents.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 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).
|
||||||
|
*/
|
||||||
|
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]
|
||||||
|
);
|
||||||
|
}
|
||||||
317
pnpm-lock.yaml
generated
317
pnpm-lock.yaml
generated
@@ -17,24 +17,45 @@ importers:
|
|||||||
|
|
||||||
apps/api:
|
apps/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@eden/config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/config
|
||||||
'@eden/database':
|
'@eden/database':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/database
|
version: link:../../packages/database
|
||||||
|
'@eden/domain-shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/domain-shared
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^10.4.15
|
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':
|
'@nestjs/core':
|
||||||
specifier: ^10.4.15
|
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':
|
'@nestjs/platform-express':
|
||||||
specifier: ^10.4.15
|
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':
|
'@nestjs/terminus':
|
||||||
specifier: ^10.2.3
|
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)
|
||||||
|
argon2:
|
||||||
|
specifier: ^0.41.1
|
||||||
|
version: 0.41.1
|
||||||
|
class-transformer:
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1
|
||||||
|
class-validator:
|
||||||
|
specifier: ^0.14.1
|
||||||
|
version: 0.14.4
|
||||||
express:
|
express:
|
||||||
specifier: ^4.21.2
|
specifier: ^4.21.2
|
||||||
version: 4.22.2(supports-color@7.2.0)
|
version: 4.22.2(supports-color@7.2.0)
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.3
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -45,6 +66,9 @@ importers:
|
|||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.7
|
||||||
|
version: 9.0.10
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.5
|
specifier: ^22.10.5
|
||||||
version: 22.20.1
|
version: 22.20.1
|
||||||
@@ -180,7 +204,14 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|
||||||
packages/config:
|
packages/config:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: ^3.24.1
|
||||||
|
version: 3.25.76
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.10.5
|
||||||
|
version: 22.20.1
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
@@ -197,6 +228,9 @@ importers:
|
|||||||
specifier: ^8.13.1
|
specifier: ^8.13.1
|
||||||
version: 8.23.0
|
version: 8.23.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^22.10.5
|
||||||
|
version: 22.20.1
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.11.10
|
specifier: ^8.11.10
|
||||||
version: 8.23.1
|
version: 8.23.1
|
||||||
@@ -672,6 +706,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@microsoft/tsdoc@0.15.1':
|
||||||
|
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
|
||||||
|
|
||||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||||
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
|
resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==}
|
||||||
engines: {node: ^22.20 || ^24.12 || >=25}
|
engines: {node: ^22.20 || ^24.12 || >=25}
|
||||||
@@ -709,12 +746,42 @@ packages:
|
|||||||
'@nestjs/websockets':
|
'@nestjs/websockets':
|
||||||
optional: true
|
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':
|
'@nestjs/platform-express@10.4.22':
|
||||||
resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==}
|
resolution: {integrity: sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nestjs/common': ^10.0.0
|
'@nestjs/common': ^10.0.0
|
||||||
'@nestjs/core': ^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':
|
'@nestjs/terminus@10.3.0':
|
||||||
resolution: {integrity: sha512-vOJGCwt1OgrFuuxWQwPoaHqy9m9CfIk2qMUX2mosZLK5dFVJSEjHXrklkh3/Fw9PiUnfzvYFfiAdJRzUaxx+5Q==}
|
resolution: {integrity: sha512-vOJGCwt1OgrFuuxWQwPoaHqy9m9CfIk2qMUX2mosZLK5dFVJSEjHXrklkh3/Fw9PiUnfzvYFfiAdJRzUaxx+5Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -780,6 +847,10 @@ packages:
|
|||||||
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
|
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@phc/format@1.0.0':
|
||||||
|
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||||
|
|
||||||
@@ -921,6 +992,9 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@scarf/scarf@1.4.0':
|
||||||
|
resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.2.7':
|
'@tokenizer/inflate@0.2.7':
|
||||||
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
|
resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -988,6 +1062,12 @@ packages:
|
|||||||
'@types/http-errors@2.0.5':
|
'@types/http-errors@2.0.5':
|
||||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@22.20.1':
|
'@types/node@22.20.1':
|
||||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||||
|
|
||||||
@@ -1017,6 +1097,9 @@ packages:
|
|||||||
'@types/serve-static@2.2.0':
|
'@types/serve-static@2.2.0':
|
||||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
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':
|
'@vitejs/plugin-react@4.7.0':
|
||||||
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==}
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
@@ -1051,6 +1134,13 @@ packages:
|
|||||||
arg@5.0.2:
|
arg@5.0.2:
|
||||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||||
|
|
||||||
|
argon2@0.41.1:
|
||||||
|
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
|
||||||
|
engines: {node: '>=16.17.0'}
|
||||||
|
|
||||||
|
argparse@2.0.1:
|
||||||
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
array-flatten@1.1.1:
|
array-flatten@1.1.1:
|
||||||
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
|
||||||
|
|
||||||
@@ -1099,6 +1189,9 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@@ -1141,6 +1234,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
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:
|
cli-boxes@2.2.1:
|
||||||
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
|
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1236,6 +1335,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@@ -1453,6 +1555,10 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
js-yaml@4.1.0:
|
||||||
|
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jsesc@3.1.0:
|
jsesc@3.1.0:
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -1463,6 +1569,19 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
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==}
|
||||||
|
|
||||||
|
libphonenumber-js@1.13.12:
|
||||||
|
resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==}
|
||||||
|
|
||||||
lilconfig@3.1.3:
|
lilconfig@3.1.3:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1470,6 +1589,30 @@ packages:
|
|||||||
lines-and-columns@1.2.4:
|
lines-and-columns@1.2.4:
|
||||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||||
|
|
||||||
|
lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
|
||||||
|
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.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
|
lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -1554,6 +1697,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-addon-api@8.9.2:
|
||||||
|
resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==}
|
||||||
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
@@ -1563,6 +1710,10 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4:
|
||||||
|
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-pg-migrate@7.9.1:
|
node-pg-migrate@7.9.1:
|
||||||
resolution: {integrity: sha512-6z4OSN27ye8aYdX9ZU7NN2PTI5pOp34hTr+22Ej12djIYECq++gT7LPLZVOQXEeVCBOZQLqf87kC3Y36G434OQ==}
|
resolution: {integrity: sha512-6z4OSN27ye8aYdX9ZU7NN2PTI5pOp34hTr+22Ej12djIYECq++gT7LPLZVOQXEeVCBOZQLqf87kC3Y36G434OQ==}
|
||||||
engines: {node: '>=18.19.0'}
|
engines: {node: '>=18.19.0'}
|
||||||
@@ -1821,6 +1972,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.8.5:
|
||||||
|
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
send@0.19.2:
|
send@0.19.2:
|
||||||
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1904,6 +2060,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
swagger-ui-dist@5.18.2:
|
||||||
|
resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==}
|
||||||
|
|
||||||
tailwindcss@3.4.19:
|
tailwindcss@3.4.19:
|
||||||
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
|
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -1994,6 +2153,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
|
validator@13.15.35:
|
||||||
|
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2076,6 +2239,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
|
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.3.0': {}
|
'@alloc/quick-lru@5.3.0': {}
|
||||||
@@ -2373,10 +2539,12 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
||||||
|
'@microsoft/tsdoc@0.15.1': {}
|
||||||
|
|
||||||
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
'@napi-rs/lzma-linux-x64-gnu@1.5.1':
|
||||||
optional: true
|
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:
|
dependencies:
|
||||||
file-type: 20.4.1(supports-color@7.2.0)
|
file-type: 20.4.1(supports-color@7.2.0)
|
||||||
iterare: 1.2.1
|
iterare: 1.2.1
|
||||||
@@ -2384,12 +2552,15 @@ snapshots:
|
|||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
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
|
'@nuxtjs/opencollective': 0.3.2
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
iterare: 1.2.1
|
iterare: 1.2.1
|
||||||
@@ -2399,14 +2570,22 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
optionalDependencies:
|
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:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- 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:
|
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)
|
||||||
'@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)
|
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)
|
body-parser: 1.20.4(supports-color@7.2.0)
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
express: 4.22.1(supports-color@7.2.0)
|
express: 4.22.1(supports-color@7.2.0)
|
||||||
@@ -2415,10 +2594,25 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0)
|
'@microsoft/tsdoc': 0.15.1
|
||||||
'@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)
|
||||||
|
'@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
|
boxen: 5.1.2
|
||||||
check-disk-space: 3.4.0
|
check-disk-space: 3.4.0
|
||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
@@ -2444,6 +2638,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|
||||||
|
'@phc/format@1.0.0': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.63.1':
|
'@rollup/rollup-android-arm-eabi@4.63.1':
|
||||||
@@ -2521,6 +2717,8 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc@4.63.1':
|
'@rollup/rollup-win32-x64-msvc@4.63.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@scarf/scarf@1.4.0': {}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.2.7(supports-color@7.2.0)':
|
'@tokenizer/inflate@0.2.7(supports-color@7.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@7.2.0)
|
debug: 4.4.3(supports-color@7.2.0)
|
||||||
@@ -2596,6 +2794,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/http-errors@2.0.5': {}
|
'@types/http-errors@2.0.5': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 22.20.1
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@22.20.1':
|
'@types/node@22.20.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
@@ -2630,6 +2835,8 @@ snapshots:
|
|||||||
'@types/http-errors': 2.0.5
|
'@types/http-errors': 2.0.5
|
||||||
'@types/node': 22.20.1
|
'@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))':
|
'@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:
|
dependencies:
|
||||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||||
@@ -2668,6 +2875,14 @@ snapshots:
|
|||||||
|
|
||||||
arg@5.0.2: {}
|
arg@5.0.2: {}
|
||||||
|
|
||||||
|
argon2@0.41.1:
|
||||||
|
dependencies:
|
||||||
|
'@phc/format': 1.0.0
|
||||||
|
node-addon-api: 8.9.2
|
||||||
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
|
argparse@2.0.1: {}
|
||||||
|
|
||||||
array-flatten@1.1.1: {}
|
array-flatten@1.1.1: {}
|
||||||
|
|
||||||
autoprefixer@10.5.4(postcss@8.5.26):
|
autoprefixer@10.5.4(postcss@8.5.26):
|
||||||
@@ -2746,6 +2961,8 @@ snapshots:
|
|||||||
node-releases: 2.0.54
|
node-releases: 2.0.54
|
||||||
update-browserslist-db: 1.3.2(browserslist@4.28.8)
|
update-browserslist-db: 1.3.2(browserslist@4.28.8)
|
||||||
|
|
||||||
|
buffer-equal-constant-time@1.0.1: {}
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
busboy@1.6.0:
|
busboy@1.6.0:
|
||||||
@@ -2789,6 +3006,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
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: {}
|
cli-boxes@2.2.1: {}
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
@@ -2867,6 +3092,10 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
|
ecdsa-sig-formatter@1.0.11:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.420: {}
|
electron-to-chromium@1.5.420: {}
|
||||||
@@ -3177,14 +3406,60 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
js-yaml@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
argparse: 2.0.1
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
|
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.8.5
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
libphonenumber-js@1.13.12: {}
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|
||||||
|
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: {}
|
||||||
|
|
||||||
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
loose-envify@1.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
@@ -3254,10 +3529,14 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
|
node-addon-api@8.9.2: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
|
node-gyp-build@4.8.4: {}
|
||||||
|
|
||||||
node-pg-migrate@7.9.1(@types/pg@8.23.1)(pg@8.23.0):
|
node-pg-migrate@7.9.1(@types/pg@8.23.1)(pg@8.23.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 11.0.3
|
glob: 11.0.3
|
||||||
@@ -3500,6 +3779,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
semver@7.8.5: {}
|
||||||
|
|
||||||
send@0.19.2(supports-color@7.2.0):
|
send@0.19.2(supports-color@7.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9(supports-color@7.2.0)
|
debug: 2.6.9(supports-color@7.2.0)
|
||||||
@@ -3607,6 +3888,10 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
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):
|
tailwindcss@3.4.19(tsx@4.23.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.3.0
|
'@alloc/quick-lru': 5.3.0
|
||||||
@@ -3712,6 +3997,8 @@ snapshots:
|
|||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
|
validator@13.15.35: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13):
|
vite@6.4.3(@types/node@22.20.1)(jiti@1.21.7)(tsx@4.23.13):
|
||||||
@@ -3766,3 +4053,5 @@ snapshots:
|
|||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
y18n: 5.0.8
|
y18n: 5.0.8
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
|
|||||||
@@ -3,4 +3,6 @@ packages:
|
|||||||
- "packages/*"
|
- "packages/*"
|
||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@nestjs/core': true
|
'@nestjs/core': true
|
||||||
|
'@scarf/scarf': false
|
||||||
|
argon2: true
|
||||||
esbuild: true
|
esbuild: true
|
||||||
|
|||||||
Reference in New Issue
Block a user