Cambio Forzado de Contraseña
Sección titulada «Cambio Forzado de Contraseña»Sistema que obliga a los usuarios a cambiar su contraseña cuando es establecida por un administrador.
🎯 Casos de Uso
Sección titulada «🎯 Casos de Uso»- Admin crea usuario nuevo → Usuario debe cambiar contraseña en primer login
- Admin cambia contraseña de usuario → Usuario debe cambiar contraseña en próximo login
- Usuario cambia su propia contraseña → Flag se limpia automáticamente
🔄 Flujo Completo
Sección titulada «🔄 Flujo Completo»1. Admin Establece/Cambia Contraseña
Sección titulada «1. Admin Establece/Cambia Contraseña»Cuando un admin crea un usuario o cambia su contraseña desde el panel de administración:
// En updateUserByAdmin operationif (newPassword) { // ... actualiza password ... // Marca que el usuario debe cambiar su contraseña passwordWasChanged = true;}
// Al final de la operacióndata: { ...updateData, ...(passwordWasChanged && { mustChangePassword: true }),}2. Usuario Hace Login
Sección titulada «2. Usuario Hace Login»Al iniciar sesión, si mustChangePassword = true, aparece un modal no-cerrable que:
- ❌ No se puede cerrar (sin botón X, sin click fuera, sin ESC)
- 🔒 Bloquea toda la interfaz
- 📝 Muestra formulario de cambio de contraseña
- ⚠️ Muestra alerta roja explicando la situación
// En App.tsxconst mustChangePassword = user?.mustChangePassword === true;
return ( <> <ForcePasswordChangeModal open={mustChangePassword} /> {/* Resto de la app */} </>);3. Usuario Cambia Contraseña
Sección titulada «3. Usuario Cambia Contraseña»El modal contiene un formulario con:
- Contraseña actual
- Nueva contraseña
- Confirmar nueva contraseña
- Botones show/hide para cada campo
Al enviar:
// En ForcePasswordChangeModalawait changePassword({ currentPassword: data.currentPassword, newPassword: data.newPassword, confirmPassword: data.confirmPassword,});
// Después de 1.5 segundos, recarga la páginasetTimeout(() => { window.location.reload();}, 1500);4. Backend Limpia el Flag
Sección titulada «4. Backend Limpia el Flag»// En changePassword operationawait updateAuthIdentityProviderData(providerId, providerData, { hashedPassword: args.newPassword,});
// Limpia el flag mustChangePasswordawait context.entities.User.update({ where: { id: context.user!.id }, data: { mustChangePassword: false },});5. Usuario Continúa Normalmente
Sección titulada «5. Usuario Continúa Normalmente»- La página recarga con el nuevo estado del usuario
mustChangePassword = false- Modal desaparece
- Usuario tiene acceso completo a la aplicación
- Permanece en la misma página donde estaba
🗄️ Esquema de Base de Datos
Sección titulada «🗄️ Esquema de Base de Datos»model User { id String @id @default(uuid()) email String? @unique username String? @unique isAdmin Boolean @default(false) mustChangePassword Boolean @default(false) // 👈 Flag nuevo // ... otros campos}🔧 Componentes Principales
Sección titulada «🔧 Componentes Principales»ForcePasswordChangeModal.tsx
Sección titulada «ForcePasswordChangeModal.tsx»Modal React que aparece cuando mustChangePassword = true.
Props:
open: boolean- Controla si el modal está visible
Características:
- No se puede cerrar manualmente
- Bloquea eventos de teclado (ESC) y mouse (click fuera)
- Muestra alerta destructiva roja
- Formulario con validación Zod
- Botones show/hide para contraseñas
- Recarga automática después del cambio
Ubicación: src/user/components/ForcePasswordChangeModal.tsx
Integración en App.tsx
Sección titulada «Integración en App.tsx»import { ForcePasswordChangeModal } from "../user/components/ForcePasswordChangeModal";
function AppContent() { const { data: user } = useAuth(); const mustChangePassword = user?.mustChangePassword === true;
return ( <> <ForcePasswordChangeModal open={mustChangePassword} /> {/* Resto de la app */} </> );}📋 API Operations Afectadas
Sección titulada «📋 API Operations Afectadas»createUserByAdmin
Sección titulada «createUserByAdmin»Al crear un usuario, establece mustChangePassword: true por defecto:
const userFields: Partial<User> = { email: args.email, username: args.username, isAdmin: args.isAdmin ?? false, mustChangePassword: true, // 👈 Fuerza cambio en primer login subscriptionStatus: args.subscriptionStatus ?? null, subscriptionPlan: args.subscriptionPlan ?? null,};updateUserByAdmin
Sección titulada «updateUserByAdmin»Si el admin cambia la contraseña, establece mustChangePassword: true:
if (newPassword) { // ... actualiza AuthIdentity ... passwordWasChanged = true;}
return context.entities.User.update({ where: { id }, data: { ...updateData, ...(passwordWasChanged && { mustChangePassword: true }), },});changePassword
Sección titulada «changePassword»Cuando el usuario cambia su propia contraseña, limpia el flag:
await updateAuthIdentityProviderData(providerId, providerData, { hashedPassword: args.newPassword,});
await context.entities.User.update({ where: { id: context.user!.id }, data: { mustChangePassword: false }, // 👈 Limpia el flag});🔒 Seguridad
Sección titulada «🔒 Seguridad»Validación de Contraseñas
Sección titulada «Validación de Contraseñas»const passwordFormSchema = z .object({ currentPassword: z.string().min(1, "La contraseña actual es requerida"), newPassword: z.string().min(8, "La contraseña debe tener al menos 8 caracteres"), confirmPassword: z.string().min(1, "Por favor confirma tu contraseña"), }) .refine(data => data.newPassword === data.confirmPassword, { message: "Las contraseñas no coinciden", path: ["confirmPassword"], }) .refine(data => data.currentPassword !== data.newPassword, { message: "La nueva contraseña debe ser diferente de la actual", path: ["newPassword"], });Prevención de Autocomplete
Sección titulada «Prevención de Autocomplete»<Input type={showPassword ? "text" : "password"} autoComplete="new-password" // 👈 Previene autofill {...field}/>Verificación de Contraseña Actual
Sección titulada «Verificación de Contraseña Actual»Antes de cambiar, se verifica que la contraseña actual sea correcta:
const isPasswordCorrect = await verify(providerData.hashedPassword, args.currentPassword);
if (!isPasswordCorrect) { throw new HttpError(400, "Current password is incorrect");}🧪 Testing
Sección titulada «🧪 Testing»Ver Guía de Tests E2E para información sobre cómo probar este feature.
Prueba Manual Rápida
Sección titulada «Prueba Manual Rápida»- Hacer login como admin
- Ir a
/admin/users - Cambiar contraseña de un usuario
- Logout
- Login con ese usuario
- ✅ Debería aparecer el modal forzoso
- Cambiar contraseña
- ✅ Modal desaparece y app funciona normal
📝 Logging y Auditoría
Sección titulada «📝 Logging y Auditoría»Todas las operaciones de cambio de contraseña se registran:
// Cuando admin cambia passwordconsole.log(`Admin ${context.user.id} changed password for user ${id}`);
// Cuando admin crea usuarioconsole.log(`Admin ${context.user.id} created user ${newUser.id}`, { email: args.email, isAdmin: args.isAdmin,});🐛 Troubleshooting
Sección titulada «🐛 Troubleshooting»Modal no aparece después del login
Sección titulada «Modal no aparece después del login»Causa: El flag mustChangePassword no está en true
Solución: Verificar en la base de datos:
SELECT email, "mustChangePassword" FROM "User" WHERE email = 'usuario@ejemplo.com';Modal no se cierra después de cambiar contraseña
Sección titulada «Modal no se cierra después de cambiar contraseña»Causa: La operación changePassword falló o no limpió el flag
Solución:
- Verificar logs del servidor
- Verificar que la contraseña actual sea correcta
- Verificar que la nueva contraseña cumpla requisitos mínimos
AuthIdentity no existe para el usuario
Sección titulada «AuthIdentity no existe para el usuario»Causa: Usuario fue creado de forma incorrecta (sin AuthIdentity)
Solución: El código ya maneja esto automáticamente, creando la AuthIdentity si no existe:
if (!authIdentity) { // Crea Auth record si no existe let auth = await prisma.auth.findUnique({ where: { userId: id } }); if (!auth) { auth = await prisma.auth.create({ data: { userId: id } }); }
// Crea AuthIdentity await prisma.authIdentity.create({ data: { providerName: "email", providerUserId: email, providerData: JSON.stringify(providerData), authId: auth.id, }, });}✨ Mejoras Futuras
Sección titulada «✨ Mejoras Futuras»Ideas para mejorar este feature:
- Email notification cuando admin cambia contraseña
- Historial de cambios de contraseña
- Forzar cambio de contraseña periódico (ej: cada 90 días)
- Política de contraseñas más estricta (mayúsculas, números, símbolos)
- Prevenir reutilización de contraseñas anteriores
- Bloqueo temporal después de X intentos fallidos