# RF-USER-004: Cambio de Password

## Identificacion

| Campo | Valor |
|-------|-------|
| **ID** | RF-USER-004 |
| **Modulo** | MGN-002 |
| **Nombre Modulo** | Users - Gestion de Usuarios |
| **Prioridad** | P0 |
| **Complejidad** | Baja |
| **Estado** | Aprobado |
| **Autor** | System |
| **Fecha** | 2025-12-05 |

---

## Descripcion

El sistema debe permitir a los usuarios cambiar su contraseña de forma segura, requiriendo la contraseña actual para autorizar el cambio. Este proceso es diferente a la recuperacion de password (RF-AUTH-005) ya que el usuario conoce su password actual.

### Contexto de Negocio

El cambio de password es necesario para:
- Cumplir con politicas de rotacion de passwords
- Responder a sospechas de compromiso
- Buenas practicas de seguridad
- Requisitos de compliance

---

## Criterios de Aceptacion

- [x] **CA-001:** El usuario debe proporcionar su password actual para cambiar
- [x] **CA-002:** El nuevo password debe cumplir politica de complejidad
- [x] **CA-003:** El nuevo password no puede ser igual a los ultimos 5
- [x] **CA-004:** El sistema debe invalidar todas las otras sesiones (opcional)
- [x] **CA-005:** El sistema debe notificar via email del cambio
- [x] **CA-006:** El sistema debe registrar el cambio en password_history
- [x] **CA-007:** La sesion actual puede mantenerse activa

### Ejemplos de Verificacion

```gherkin
Scenario: Cambio de password exitoso
  Given un usuario autenticado
  When proporciona password actual correcto
  And nuevo password "NuevoPass123!"
  And el nuevo password cumple requisitos
  Then el sistema actualiza el password
  And guarda en password_history
  And envia email de notificacion
  And opcionalmente invalida otras sesiones
  And responde con status 200

Scenario: Password actual incorrecto
  Given un usuario autenticado
  When proporciona password actual incorrecto
  Then el sistema responde con status 400
  And el mensaje es "Password actual incorrecto"

Scenario: Nuevo password no cumple requisitos
  Given un usuario autenticado
  When el nuevo password es "abc123"
  Then el sistema responde con status 400
  And lista los requisitos no cumplidos

Scenario: Password igual a anterior
  Given un usuario que uso "MiPass123!" hace 2 meses
  When intenta cambiar a "MiPass123!" nuevamente
  Then el sistema responde con status 400
  And el mensaje es "No puedes reusar passwords anteriores"
```

---

## Reglas de Negocio

| ID | Regla | Validacion |
|----|-------|------------|
| RN-001 | Password actual requerido | Verificacion bcrypt |
| RN-002 | Nuevo password min 8 caracteres | String length |
| RN-003 | Nuevo password requiere mayuscula | Regex [A-Z] |
| RN-004 | Nuevo password requiere minuscula | Regex [a-z] |
| RN-005 | Nuevo password requiere numero | Regex [0-9] |
| RN-006 | Nuevo password requiere especial | Regex [@$!%*?&] |
| RN-007 | No reusar ultimos 5 passwords | Check password_history |
| RN-008 | Nuevo != Actual | Comparacion |

### Politica de Password

```
Requisitos minimos:
├── Longitud: 8-128 caracteres
├── Al menos 1 mayuscula (A-Z)
├── Al menos 1 minuscula (a-z)
├── Al menos 1 numero (0-9)
├── Al menos 1 caracter especial (!@#$%^&*)
├── No puede contener el email del usuario
└── No puede ser igual a los ultimos 5 passwords
```

---

## Impacto en Capas

### Database

| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Tabla | usar | `users` - actualizar password_hash |
| Tabla | usar | `password_history` - registrar cambio |
| Tabla | usar | `session_history` - registrar evento |

### Backend

| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Controller | agregar | `UsersController.changePassword()` |
| Method | crear | `UsersService.changePassword()` |
| Method | usar | `PasswordService.validatePolicy()` |
| Method | usar | `PasswordService.checkHistory()` |
| DTO | crear | `ChangePasswordDto` |
| Endpoint | crear | `POST /api/v1/users/me/password` |

### Frontend

| Elemento | Accion | Descripcion |
|----------|--------|-------------|
| Componente | crear | `ChangePasswordForm` |
| Componente | usar | `PasswordStrengthIndicator` |
| Pagina | agregar | Seccion en ProfilePage |

---

## Dependencias

### Depende de (Bloqueantes)

| ID | Requerimiento | Estado |
|----|---------------|--------|
| RF-USER-001 | CRUD Usuarios | Tabla users |
| RF-AUTH-001 | Login | Autenticacion |

### Reutiliza de

| ID | Requerimiento | Elementos |
|----|---------------|-----------|
| RF-AUTH-005 | Password Recovery | password_history, validaciones |

---

## Especificaciones Tecnicas

### Endpoint POST /api/v1/users/me/password

```typescript
// Request
{
  "currentPassword": "MiPasswordActual123!",
  "newPassword": "MiNuevoPassword456!",
  "confirmPassword": "MiNuevoPassword456!",
  "logoutOtherSessions": true  // opcional, default false
}

// Response 200
{
  "message": "Password actualizado exitosamente",
  "sessionsInvalidated": 2  // si logoutOtherSessions = true
}

// Response 400 - Password actual incorrecto
{
  "statusCode": 400,
  "message": "Password actual incorrecto"
}

// Response 400 - No cumple politica
{
  "statusCode": 400,
  "message": "El password no cumple los requisitos",
  "errors": [
    "Debe incluir al menos una mayuscula",
    "Debe incluir al menos un caracter especial"
  ]
}

// Response 400 - Password reutilizado
{
  "statusCode": 400,
  "message": "No puedes usar un password que hayas usado anteriormente"
}
```

### Implementacion del Service

```typescript
// users.service.ts
async changePassword(
  userId: string,
  dto: ChangePasswordDto,
): Promise<ChangePasswordResult> {
  const user = await this.usersRepository.findOne({ where: { id: userId } });

  // 1. Verificar password actual
  const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash);
  if (!isCurrentValid) {
    throw new BadRequestException('Password actual incorrecto');
  }

  // 2. Verificar que nuevo != actual
  if (dto.currentPassword === dto.newPassword) {
    throw new BadRequestException('El nuevo password debe ser diferente al actual');
  }

  // 3. Validar confirmacion
  if (dto.newPassword !== dto.confirmPassword) {
    throw new BadRequestException('Los passwords no coinciden');
  }

  // 4. Validar politica de password
  const policyErrors = this.passwordService.validatePolicy(dto.newPassword, user.email);
  if (policyErrors.length > 0) {
    throw new BadRequestException({
      message: 'El password no cumple los requisitos',
      errors: policyErrors,
    });
  }

  // 5. Verificar historial
  const isReused = await this.passwordService.isPasswordReused(userId, dto.newPassword);
  if (isReused) {
    throw new BadRequestException('No puedes usar un password que hayas usado anteriormente');
  }

  // 6. Hashear nuevo password
  const newHash = await bcrypt.hash(dto.newPassword, 12);

  // 7. Guardar en historial
  await this.passwordHistoryRepository.save({
    userId,
    tenantId: user.tenantId,
    passwordHash: newHash,
  });

  // 8. Actualizar usuario
  await this.usersRepository.update(userId, {
    passwordHash: newHash,
    updatedBy: userId,
  });

  // 9. Registrar evento
  await this.sessionHistoryRepository.save({
    userId,
    tenantId: user.tenantId,
    action: 'password_change',
  });

  // 10. Invalidar otras sesiones si se solicita
  let sessionsInvalidated = 0;
  if (dto.logoutOtherSessions) {
    sessionsInvalidated = await this.tokenService.revokeAllUserTokens(
      userId,
      'password_change',
    );
  }

  // 11. Enviar email de notificacion
  await this.emailService.sendPasswordChangedEmail(user.email, user.firstName);

  return {
    message: 'Password actualizado exitosamente',
    sessionsInvalidated,
  };
}
```

### Validacion de Politica

```typescript
// password.service.ts
validatePolicy(password: string, email: string): string[] {
  const errors: string[] = [];

  if (password.length < 8) {
    errors.push('Debe tener al menos 8 caracteres');
  }
  if (password.length > 128) {
    errors.push('No puede exceder 128 caracteres');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Debe incluir al menos una mayuscula');
  }
  if (!/[a-z]/.test(password)) {
    errors.push('Debe incluir al menos una minuscula');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Debe incluir al menos un numero');
  }
  if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) {
    errors.push('Debe incluir al menos un caracter especial');
  }
  if (password.toLowerCase().includes(email.split('@')[0].toLowerCase())) {
    errors.push('No puede contener tu nombre de usuario');
  }

  return errors;
}
```

---

## Datos de Prueba

| Escenario | Entrada | Resultado |
|-----------|---------|-----------|
| Cambio exitoso | currentPassword correcto, newPassword valido | 200, actualizado |
| Password actual incorrecto | currentPassword erroneo | 400, "Password actual incorrecto" |
| Passwords no coinciden | newPassword != confirmPassword | 400, "No coinciden" |
| Password muy corto | newPassword: "Ab1!" | 400, "Min 8 caracteres" |
| Sin mayuscula | newPassword: "password123!" | 400, "Requiere mayuscula" |
| Password reutilizado | newPassword en historial | 400, "No reusar" |
| Con logout otras sesiones | logoutOtherSessions: true | 200, sesiones cerradas |

---

## Estimacion

| Capa | Story Points | Notas |
|------|--------------|-------|
| Database | 0 | Usa tablas existentes |
| Backend | 2 | Endpoint + validaciones |
| Frontend | 2 | Form + strength indicator |
| **Total** | **4** | |

---

## Notas Adicionales

- Considerar forzar cambio de password cada N dias (configurable)
- Implementar indicador de fuerza de password en tiempo real
- Rate limiting: max 3 intentos por hora
- Log detallado para auditoria de seguridad
- Considerar 2FA antes de cambio de password (futuro)

---

## Historial de Cambios

| Version | Fecha | Autor | Cambios |
|---------|-------|-------|---------|
| 1.0 | 2025-12-05 | System | Creacion inicial |

---

## Aprobaciones

| Rol | Nombre | Fecha | Firma |
|-----|--------|-------|-------|
| Analista | System | 2025-12-05 | [x] |
| Tech Lead | - | - | [ ] |
| Product Owner | - | - | [ ] |
