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)}
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,