diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 3a8b56e..20a7a00 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -45,6 +45,10 @@ import metricasRoutes from './routes/metricas.routes.js'; const app: Express = express(); +// Trust proxy — la app corre detrás de Cloudflare/nginx. Necesario para que +// express-rate-limit lea correctamente X-Forwarded-For sin lanzar warnings. +app.set('trust proxy', 1); + // Security. Helmet default incluye un CSP restrictivo que puede chocar con el // frontend cuando éste embebe recursos propios (ej: /terminos embebe el PDF de // /legal/). Dejamos CSP off en el API y centralizamos los headers de seguridad diff --git a/apps/api/src/services/email/email.service.ts b/apps/api/src/services/email/email.service.ts index 98d276c..517d890 100644 --- a/apps/api/src/services/email/email.service.ts +++ b/apps/api/src/services/email/email.service.ts @@ -18,7 +18,7 @@ async function sendEmail(to: string, subject: string, html: string) { } export const emailService = { - sendWelcome: async (to: string, data: { nombre: string; email: string; tempPassword: string }) => { + sendWelcome: async (to: string, data: { nombre: string; email: string; tempPassword: string | null | undefined }) => { const { welcomeEmail } = await import('./templates/welcome.js'); await sendEmail(to, 'Bienvenido a Horux360', welcomeEmail(data)); }, @@ -60,7 +60,7 @@ export const emailService = { clienteRfc: string; adminEmail: string; adminNombre: string; - tempPassword: string; + tempPassword: string | null | undefined; databaseName: string; plan: string; }) => { diff --git a/apps/api/src/services/email/templates/new-client-admin.ts b/apps/api/src/services/email/templates/new-client-admin.ts index d9894f8..7941ce1 100644 --- a/apps/api/src/services/email/templates/new-client-admin.ts +++ b/apps/api/src/services/email/templates/new-client-admin.ts @@ -25,7 +25,7 @@ export function newClientAdminEmail(data: { clienteRfc: string; adminEmail: string; adminNombre: string; - tempPassword: string; + tempPassword: string | null | undefined; databaseName: string; plan: string; }): string { @@ -46,7 +46,7 @@ export function newClientAdminEmail(data: { ${sectionHeader('Credenciales del usuario', C.secondary)} ${row('Nombre', escapeHtml(data.adminNombre))} ${row('Email', `${escapeHtml(data.adminEmail)}`)} - ${row('Contraseña temporal', `${escapeHtml(data.tempPassword)}`, true)} + ${row('Contraseña temporal', `${escapeHtml(data.tempPassword || 'N/A - usuario ya existía')}`, true)}
diff --git a/apps/api/src/services/email/templates/welcome.ts b/apps/api/src/services/email/templates/welcome.ts index 279025f..0e717ab 100644 --- a/apps/api/src/services/email/templates/welcome.ts +++ b/apps/api/src/services/email/templates/welcome.ts @@ -1,6 +1,6 @@ import { baseTemplate, heading, primaryButton, infoBox, BRAND_COLORS as C } from './base.js'; -export function welcomeEmail(data: { nombre: string; email: string; tempPassword: string }): string { +export function welcomeEmail(data: { nombre: string; email: string; tempPassword: string | null | undefined }): string { return baseTemplate(` ${heading('Bienvenido a Horux 360')}

Hola ${data.nombre},

diff --git a/apps/api/src/services/tenants.service.ts b/apps/api/src/services/tenants.service.ts index 25ed8d7..256ce5a 100644 --- a/apps/api/src/services/tenants.service.ts +++ b/apps/api/src/services/tenants.service.ts @@ -91,26 +91,41 @@ export async function createTenant(data: { } }); - // 3. Create admin user with temp password - const tempPassword = randomBytes(4).toString('hex'); // 8-char random - const hashedPassword = await bcrypt.hash(tempPassword, 10); - - // Get owner role ID from roles table (rol que asignamos al dueño del tenant al crearlo) + // 3. Create or reuse admin user const ownerRol = await prisma.rol.findUnique({ where: { nombre: 'owner' } }); if (!ownerRol) throw new Error('Rol owner no encontrado en la base de datos'); - const user = await prisma.user.create({ - data: { - email: data.adminEmail, - passwordHash: hashedPassword, - nombre: data.adminNombre, - lastTenantId: tenant.id, - }, - }); + let user = await prisma.user.findUnique({ where: { email: data.adminEmail } }); + let tempPassword: string | null = null; - // Crea membership owner del nuevo user en su tenant (fase 4 multi-tenant) - await prisma.tenantMembership.create({ - data: { + if (!user) { + tempPassword = randomBytes(4).toString('hex'); // 8-char random + const hashedPassword = await bcrypt.hash(tempPassword, 10); + user = await prisma.user.create({ + data: { + email: data.adminEmail, + passwordHash: hashedPassword, + nombre: data.adminNombre, + lastTenantId: tenant.id, + }, + }); + } else { + // User ya existe: actualizar lastTenantId y nombre si cambió + await prisma.user.update({ + where: { id: user.id }, + data: { + lastTenantId: tenant.id, + ...(data.adminNombre && data.adminNombre !== user.nombre ? { nombre: data.adminNombre } : {}), + }, + }); + } + + // Crea membership owner del user en su tenant (fase 4 multi-tenant). + // Si ya existía (re-invite a otro tenant), reactivar. + await prisma.tenantMembership.upsert({ + where: { userId_tenantId: { userId: user.id, tenantId: tenant.id } }, + update: { rolId: ownerRol.id, isOwner: true, active: true }, + create: { userId: user.id, tenantId: tenant.id, rolId: ownerRol.id,