feat: Fase 1 — Auth JWT+MFA, portales CRUD, UI completa

API:
- AuthModule: register, login, refresh, logout, MFA/TOTP setup+verify
- JwtStrategy + JwtAuthGuard + RolesGuard + CurrentUser decorator
- PackagesModule: CRUD paquetes + historial de estados
- PreAlertsModule: pre-alertas por usuario
- UsersModule: gestión de usuarios + roles + activación
- B2BModule: solicitudes de carga pesada/cotización
- ValidationPipe global + CORS configurado

Web (Next.js 15):
- globals.css completo (design system + utility classes)
- Layout raíz con WhatsApp flotante
- /login + /registro funcionales con JWT y redirección por rol
- /portal: dashboard, mi-casillero, mis-paquetes, pre-alerta, calculadora, perfil
- /admin: dashboard, usuarios (gestión roles/activación), tarifas, reportes, auditoría
- /bodega: dashboard, paquetes (crear+actualizar estado), verificación, despacho
- /tracking: tracking real con progreso visual + historial
- /calculadora: calculadora interactiva real (API SENAE §15)
- /como-funciona, /tarifas, /quienes-somos, /casillero
- /carga-pesada + /carga-pesada/cotizacion (formulario B2B)
- lib/api.ts: cliente HTTP con auto-refresh de token

Roles sincronizados con schema: SUPER_ADMIN, ADMIN_EMPRESA,
OPERADOR_BODEGA, AGENTE_ADUANERO, CLIENTE, SOPORTE
This commit is contained in:
Lizandro Guarnizo
2026-06-01 09:42:38 -05:00
parent 93db76897d
commit 4872053fd1
57 changed files with 4330 additions and 192 deletions
+43
View File
@@ -0,0 +1,43 @@
import { Controller, Get, Patch, Body, Param, Query, UseGuards } from "@nestjs/common";
import { UsersService } from "./users.service";
import { JwtAuthGuard, RolesGuard, Roles } from "../auth/guards/auth.guard";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
import { IsEnum, IsBoolean } from "class-validator";
class UpdateRoleDto {
@IsEnum(["CLIENTE","OPERADOR_BODEGA","AGENTE_ADUANERO","ADMIN_EMPRESA","SUPER_ADMIN","SOPORTE"])
role!: string;
}
class SetActiveDto {
@IsBoolean() isActive!: boolean;
}
@Controller("users")
@UseGuards(JwtAuthGuard, RolesGuard)
export class UsersController {
constructor(private svc: UsersService) {}
@Get()
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
findAll(@CurrentUser() user: any, @Query("search") search?: string): Promise<any[]> {
return this.svc.findAll(user.tenantId, search);
}
@Get(":id")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
findOne(@Param("id") id: string): Promise<any> {
return this.svc.findOne(id);
}
@Patch(":id/role")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
updateRole(@Param("id") id: string, @Body() dto: UpdateRoleDto): Promise<any> {
return this.svc.updateRole(id, dto.role);
}
@Patch(":id/active")
@Roles("ADMIN_EMPRESA", "SUPER_ADMIN")
setActive(@Param("id") id: string, @Body() dto: SetActiveDto): Promise<any> {
return this.svc.setActive(id, dto.isActive);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { Module } from "@nestjs/common";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
@Module({ controllers: [UsersController], providers: [UsersService] })
export class UsersModule {}
+49
View File
@@ -0,0 +1,49 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
async findAll(tenantId: string, search?: string): Promise<any[]> {
return this.prisma.client.user.findMany({
where: {
tenantId,
...(search ? {
OR: [
{ email: { contains: search, mode: "insensitive" } },
{ firstName: { contains: search, mode: "insensitive" } },
{ lastName: { contains: search, mode: "insensitive" } },
],
} : {}),
},
select: {
id: true, email: true, firstName: true, lastName: true, phone: true,
role: true, isActive: true, mfaEnabled: true, lastLoginAt: true, createdAt: true,
suite: { select: { code: true } },
},
orderBy: { createdAt: "desc" },
});
}
async findOne(id: string): Promise<any> {
const user = await this.prisma.client.user.findUnique({
where: { id },
select: {
id: true, email: true, firstName: true, lastName: true, phone: true,
role: true, isActive: true, mfaEnabled: true, lastLoginAt: true, createdAt: true,
suite: { select: { code: true } },
},
});
if (!user) throw new NotFoundException("Usuario no encontrado.");
return user;
}
async updateRole(id: string, role: string): Promise<any> {
return this.prisma.client.user.update({ where: { id }, data: { role: role as any } });
}
async setActive(id: string, isActive: boolean): Promise<any> {
return this.prisma.client.user.update({ where: { id }, data: { isActive } });
}
}