template-saas/apps/backend/dist/modules/users/services/invitation.service.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

238 lines
10 KiB
JavaScript

"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var InvitationService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.InvitationService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const config_1 = require("@nestjs/config");
const crypto = __importStar(require("crypto"));
const invitation_entity_1 = require("../entities/invitation.entity");
const user_entity_1 = require("../../auth/entities/user.entity");
const tenant_entity_1 = require("../../tenants/entities/tenant.entity");
const email_service_1 = require("../../email/services/email.service");
let InvitationService = InvitationService_1 = class InvitationService {
constructor(invitationRepository, userRepository, tenantRepository, emailService, configService) {
this.invitationRepository = invitationRepository;
this.userRepository = userRepository;
this.tenantRepository = tenantRepository;
this.emailService = emailService;
this.configService = configService;
this.logger = new common_1.Logger(InvitationService_1.name);
this.invitationExpirationDays = 7;
}
async invite(dto, inviterId, tenantId) {
const email = dto.email.toLowerCase().trim();
const existingUser = await this.userRepository.findOne({
where: { email, tenant_id: tenantId },
});
if (existingUser) {
throw new common_1.ConflictException('Este email ya está registrado en la organización');
}
const existingInvitation = await this.invitationRepository.findOne({
where: {
email,
tenant_id: tenantId,
status: 'pending',
},
});
if (existingInvitation) {
throw new common_1.ConflictException('Ya existe una invitación pendiente para este email');
}
const [inviter, tenant] = await Promise.all([
this.userRepository.findOne({ where: { id: inviterId } }),
this.tenantRepository.findOne({ where: { id: tenantId } }),
]);
if (!inviter) {
throw new common_1.NotFoundException('Usuario invitador no encontrado');
}
if (!tenant) {
throw new common_1.NotFoundException('Organización no encontrada');
}
const token = this.generateSecureToken();
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + this.invitationExpirationDays);
const invitation = this.invitationRepository.create({
tenant_id: tenantId,
email,
token,
status: 'pending',
expires_at: expiresAt,
created_by: inviterId,
message: dto.message || null,
metadata: { role: dto.role },
});
await this.invitationRepository.save(invitation);
await this.sendInvitationEmail(invitation, inviter, tenant, dto.role);
this.logger.log(`Invitation sent to ${email} for tenant ${tenant.name}`);
return this.toResponseDto(invitation, dto.role);
}
async findAllByTenant(tenantId) {
await this.expireOldInvitations(tenantId);
const invitations = await this.invitationRepository.find({
where: { tenant_id: tenantId },
order: { created_at: 'DESC' },
});
return invitations.map((inv) => this.toResponseDto(inv, inv.metadata?.role || 'member'));
}
async resend(token, inviterId, tenantId) {
const invitation = await this.invitationRepository.findOne({
where: { token, tenant_id: tenantId },
});
if (!invitation) {
throw new common_1.NotFoundException('Invitación no encontrada');
}
if (invitation.status !== 'pending') {
throw new common_1.BadRequestException('Solo se pueden reenviar invitaciones pendientes');
}
const [inviter, tenant] = await Promise.all([
this.userRepository.findOne({ where: { id: inviterId } }),
this.tenantRepository.findOne({ where: { id: tenantId } }),
]);
if (!inviter || !tenant) {
throw new common_1.NotFoundException('Usuario o organización no encontrada');
}
const newToken = this.generateSecureToken();
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + this.invitationExpirationDays);
invitation.token = newToken;
invitation.expires_at = expiresAt;
await this.invitationRepository.save(invitation);
const role = invitation.metadata?.role || 'member';
await this.sendInvitationEmail(invitation, inviter, tenant, role);
this.logger.log(`Invitation resent to ${invitation.email}`);
return this.toResponseDto(invitation, role);
}
async cancel(id, tenantId) {
const invitation = await this.invitationRepository.findOne({
where: { id, tenant_id: tenantId },
});
if (!invitation) {
throw new common_1.NotFoundException('Invitación no encontrada');
}
if (invitation.status !== 'pending') {
throw new common_1.BadRequestException('Solo se pueden cancelar invitaciones pendientes');
}
await this.invitationRepository.remove(invitation);
this.logger.log(`Invitation ${id} cancelled for ${invitation.email}`);
}
async findByToken(token) {
const invitation = await this.invitationRepository.findOne({
where: { token },
});
if (!invitation) {
return null;
}
if (invitation.expires_at < new Date() && invitation.status === 'pending') {
invitation.status = 'expired';
await this.invitationRepository.save(invitation);
}
return invitation;
}
generateSecureToken() {
return crypto.randomBytes(32).toString('hex');
}
async sendInvitationEmail(invitation, inviter, tenant, role) {
const appUrl = this.configService.get('APP_URL', 'http://localhost:3000');
const inviteUrl = `${appUrl}/invite/${invitation.token}`;
const expiresIn = `${this.invitationExpirationDays} días`;
await this.emailService.sendTemplateEmail({
to: { email: invitation.email },
templateKey: 'invitation',
variables: {
inviterName: inviter.fullName || inviter.email,
tenantName: tenant.name,
role: this.translateRole(role),
inviteLink: inviteUrl,
expiresIn,
appName: this.configService.get('APP_NAME', 'Template SaaS'),
},
});
}
async expireOldInvitations(tenantId) {
await this.invitationRepository
.createQueryBuilder()
.update(invitation_entity_1.Invitation)
.set({ status: 'expired' })
.where('tenant_id = :tenantId', { tenantId })
.andWhere('status = :status', { status: 'pending' })
.andWhere('expires_at < NOW()')
.execute();
}
translateRole(role) {
const translations = {
admin: 'Administrador',
member: 'Miembro',
viewer: 'Visor',
};
return translations[role] || role;
}
toResponseDto(invitation, role) {
return {
id: invitation.id,
email: invitation.email,
role,
status: invitation.status,
expires_at: invitation.expires_at,
created_at: invitation.created_at,
};
}
};
exports.InvitationService = InvitationService;
exports.InvitationService = InvitationService = InvitationService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(invitation_entity_1.Invitation)),
__param(1, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
__param(2, (0, typeorm_1.InjectRepository)(tenant_entity_1.Tenant)),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository,
email_service_1.EmailService,
config_1.ConfigService])
], InvitationService);
//# sourceMappingURL=invitation.service.js.map