feat: Stripe Checkout, Zeptomail, pre-alert linking, perfil 3-tab, carga-pesada landing
- payments: real Stripe Checkout Session (redirect flow), confirmBySession, webhook handler - notifications: Zeptomail REST email, wa.me WhatsApp links, IntegrationsService injection - auth: changePassword, updateProfile, disableMfa (TOTP-verified) endpoints - packages: tryLinkPreAlert() auto-links on create (non-blocking) - integrations: catalog updated (zeptomail, stripe_webhook_secret; removed sendgrid/whatsapp-api) - web/portal/perfil: 3-tab layout (datos / contraseña / MFA) - web/portal/pago: Stripe redirect + ?success=1&session_id= callback, cancelled banner - web/admin/b2b: fix enum values (EN_COTIZACION, ACEPTADO) - web/carga-pesada: rich marketing landing (FCL/LCL, 7-step process, sectors, INEN callout) - tests: fix payments.service.spec (IntegrationsService+ConfigService mocks) - tests: fix notifications.service.spec (IntegrationsService mock, phone in mockUser) - all 104 tests passing, API + web builds clean
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
Controller, Post, Get, Body, Req, UseGuards, HttpCode, HttpStatus,
|
||||
Controller, Post, Get, Patch, Body, Req, UseGuards, HttpCode, HttpStatus,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { RegisterDto, LoginDto, RefreshDto, SetupMfaDto } from "./dto/auth.dto";
|
||||
import { RegisterDto, LoginDto, RefreshDto, SetupMfaDto, ChangePasswordDto, UpdateProfileDto } from "./dto/auth.dto";
|
||||
import { JwtAuthGuard } from "./guards/auth.guard";
|
||||
import { CurrentUser } from "./decorators/current-user.decorator";
|
||||
|
||||
@@ -45,6 +45,22 @@ export class AuthController {
|
||||
return this.auth.getProfile(user.id);
|
||||
}
|
||||
|
||||
/** PATCH /api/auth/me — Actualizar nombre / teléfono */
|
||||
@Patch("me")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
updateProfile(@Body() dto: UpdateProfileDto, @CurrentUser() user: any) {
|
||||
return this.auth.updateProfile(user.id, dto);
|
||||
}
|
||||
|
||||
/** PATCH /api/auth/password — Cambiar contraseña */
|
||||
@Patch("password")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async changePassword(@Body() dto: ChangePasswordDto, @CurrentUser() user: any) {
|
||||
await this.auth.changePassword(user.id, dto.oldPassword, dto.newPassword);
|
||||
return { message: "Contraseña actualizada correctamente." };
|
||||
}
|
||||
|
||||
/** POST /api/auth/mfa/setup — Genera QR para TOTP */
|
||||
@Post("mfa/setup")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -58,4 +74,12 @@ export class AuthController {
|
||||
verifyMfa(@Body() dto: SetupMfaDto, @CurrentUser() user: any) {
|
||||
return this.auth.verifyMfa(user.id, dto.totpCode);
|
||||
}
|
||||
|
||||
/** POST /api/auth/mfa/disable — Desactiva MFA (requiere código TOTP) */
|
||||
@Post("mfa/disable")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
disableMfa(@Body() dto: SetupMfaDto, @CurrentUser() user: any) {
|
||||
return this.auth.disableMfa(user.id, dto.totpCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +185,51 @@ export class AuthService {
|
||||
return { mfaEnabled: true };
|
||||
}
|
||||
|
||||
// ─── Change Password ─────────────────────────────────────────
|
||||
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
|
||||
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });
|
||||
if (!user) throw new UnauthorizedException();
|
||||
|
||||
const valid = await bcrypt.compare(oldPassword, user.passwordHash);
|
||||
if (!valid) throw new BadRequestException("La contraseña actual es incorrecta.");
|
||||
|
||||
if (oldPassword === newPassword) throw new BadRequestException("La nueva contraseña debe ser diferente.");
|
||||
|
||||
const passwordHash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS);
|
||||
await this.prisma.client.user.update({ where: { id: userId }, data: { passwordHash } });
|
||||
await this.audit(user.tenantId, userId, "PASSWORD_CHANGED", "User", userId);
|
||||
}
|
||||
|
||||
// ─── Update Profile ───────────────────────────────────────────
|
||||
async updateProfile(userId: string, data: { firstName?: string; lastName?: string; phone?: string }): Promise<any> {
|
||||
const user = await this.prisma.client.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
...(data.firstName ? { firstName: data.firstName } : {}),
|
||||
...(data.lastName ? { lastName: data.lastName } : {}),
|
||||
...(data.phone !== undefined ? { phone: data.phone || null } : {}),
|
||||
},
|
||||
});
|
||||
return this.sanitizeUser(user);
|
||||
}
|
||||
|
||||
// ─── Disable MFA ─────────────────────────────────────────────
|
||||
async disableMfa(userId: string, totpCode: string): Promise<any> {
|
||||
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });
|
||||
if (!user) throw new UnauthorizedException();
|
||||
if (!user.mfaEnabled) throw new BadRequestException("MFA no está activada.");
|
||||
|
||||
const ok = totpVerify({ token: totpCode, secret: user.mfaSecret! });
|
||||
if (!ok) throw new BadRequestException("Código TOTP inválido.");
|
||||
|
||||
await this.prisma.client.user.update({
|
||||
where: { id: userId },
|
||||
data: { mfaEnabled: false, mfaSecret: null },
|
||||
});
|
||||
await this.audit(user.tenantId, userId, "MFA_DISABLED", "User", userId);
|
||||
return { mfaEnabled: false };
|
||||
}
|
||||
|
||||
// ─── Profile ─────────────────────────────────────────────────
|
||||
async getProfile(userId: string): Promise<any> {
|
||||
const user = await this.prisma.client.user.findUnique({ where: { id: userId } });
|
||||
|
||||
@@ -42,3 +42,29 @@ export class SetupMfaDto {
|
||||
@IsString()
|
||||
totpCode!: string;
|
||||
}
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@IsString()
|
||||
oldPassword!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
newPassword!: string;
|
||||
}
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
firstName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
lastName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user