# Blocage / déblocage des comptes utilisateurs

Ce document décrit le mécanisme de blocage après échecs de connexion et les moyens de déblocage, notamment la commande Artisan `lnc:unlock-user`.

## Périmètre

Le blocage/déblocage décrit ici concerne l’authentification **par mot de passe** gérée par `app/Http/Controllers/Auth/LoginController.php`.

La route `POST /login` n’est enregistrée que si `routes/web.php` calcule `$allowPasswordAuth` à `true` :
- `config('upimo.disable_saml')`
- ou `config('upimo.admin_login')`
- ou `App::environment() !== 'production'`

En production avec SAML activé (et sans `admin_login`), le flux mot de passe peut ne pas être utilisé.

## Données en base (`users`)

Une migration ajoute deux colonnes sur la table `users` :
- `failed_login_attempts` : `unsignedSmallInteger`, par défaut `0`
- `locked_at` : `timestamp` nullable

Fichier : `database/migrations/2026_05_17_000000_add_login_lock_columns_to_users_table.php`

## Modèle : `App\User`

Fichier : `app/User.php`

Le modèle expose :

- `isLocked(): bool`
  - retourne `true` si `locked_at !== null`
- `unlock(): void`
  - remet `failed_login_attempts` à `0`
  - remet `locked_at` à `null`

## Quand le compte se bloque ?

Le seuil de blocage est piloté par `config('upimo.max_failed_login_attempts', 10)` :
- défini dans `config/upimo.php`
- basé sur `env('MAX_FAILED_LOGIN_ATTEMPTS', 10)`
- valeur `0` => blocage désactivé

Fichier : `app/Http/Controllers/Auth/LoginController.php`

## Throttle Laravel UI (rate limiting) vs verrouillage persistant

En plus du verrouillage applicatif basé sur `users.locked_at`, Laravel UI fournit un mécanisme de **throttling** (limitation de débit) via le trait `Illuminate\Foundation\Auth\ThrottlesLogins` (fourni ici par `vendor/laravel/ui/auth-backend/ThrottlesLogins.php`).

Différences principales :
- **Throttle (`ThrottlesLogins`)** : blocage **temporaire** basé sur un compteur en cache (rate limiter), avec une réponse `auth.throttle` et un statut HTTP **429**.
- **Verrouillage UpImo (`locked_at`)** : blocage **persistant en base** jusqu’à déblocage (UI admin ou commande `lnc:unlock-user`).

### Paramétrage de `ThrottlesLogins`

Clé de throttling :
- `throttleKey(Request $request)` retourne : `lower(email) + '|' + ip`

Paramètres :
- `maxAttempts()` : nombre max de tentatives avant lockout (défaut **5**)
  - peut être surchargé en définissant une propriété `protected $maxAttempts = <int>;` dans le contrôleur
- `decayMinutes()` : durée du lockout (défaut **1** minute)
  - peut être surchargé en définissant une propriété `protected $decayMinutes = <int>;` dans le contrôleur

Comportement :
- `incrementLoginAttempts()` enregistre un “hit” pour `decayMinutes * 60`
- `sendLockoutResponse()` lève une `ValidationException` avec `trans('auth.throttle', ['seconds' => ..., 'minutes' => ...])` et un statut HTTP **429**

Fichier (référence) : `vendor/laravel/ui/auth-backend/ThrottlesLogins.php`

### Flow “tentative échouée”

Sur un échec de connexion, `sendFailedLoginResponse()` appelle :

- `recordFailedLoginAttempt()`

Comportement :
1. si `max_failed_login_attempts <= 0` : rien n’est fait
2. on charge l’utilisateur par email (normalisé en `lowercase`)
3. si l’utilisateur est `null` ou déjà verrouillé : on ne fait rien
4. sinon, on incrémente `failed_login_attempts`
5. si `failed_login_attempts >= maxAttempts` après incrément :
   - `locked_at` est positionné à `now()`

## Quand une connexion échouée ne peut plus aboutir ?

Avant toute tentative, `attemptLogin()` vérifie :
- si l’utilisateur existe et `isLocked()` est vrai
  - alors une `ValidationException` est levée
  - le message utilisateur provient de `__('auth.locked')`

Fichier : `app/Http/Controllers/Auth/LoginController.php`

Traduction française :
- `resources/lang/fr/auth.php` : `locked`

## Quand le compte se déverrouille automatiquement ?

Après une connexion réussie, `authenticated()` effectue une remise à zéro si nécessaire :
- si `failed_login_attempts > 0` **ou** si `isLocked()` (cas improbable vu `attemptLogin`, mais logique “sécurisée”)
  - alors `unlock()` est appelé

Conséquence :
- un compte qui n’a pas atteint le seuil de blocage redevient “propre” après un login réussi
- un compte atteint “locked” ne devrait pas pouvoir se connecter pour déclencher cet auto-déblocage (puisque `attemptLogin` bloque avant)

Fichier : `app/Http/Controllers/Auth/LoginController.php`

## Déblocage manuel : API / Admin UI

Route :
- `POST /admin/users/unlock`
- name : `admin.users.unlock`

Contrôleur :
- `app/Http/Controllers/Admin/UserController.php`

Comportement :
1. validation : `email` requis et format email
2. recherche de `User` par email (normalisé en `lowercase`)
3. si l’utilisateur n’existe pas : retour avec erreurs
4. si l’utilisateur n’est pas verrouillé : message “not locked”
5. sinon : `unlock()` puis message “user_unlocked”

UI (form) :
- `resources/views/admin/home.blade.php`

## Déblocage manuel : commande Artisan `lnc:unlock-user`

Commande :
- `php artisan lnc:unlock-user {email}`

Fichier :
- `app/Console/Commands/UnlockUser.php`

Comportement :
1. charge l’utilisateur par email (normalisé en `lowercase`)
2. si `User` introuvable :
   - sortie en erreur `User not found.`
   - code retour `1`
3. si le compte n’est pas verrouillé :
   - affiche un message d’information
   - code retour `0`
4. sinon :
   - appelle `unlock()`
   - affiche `Account unlocked: ...`
   - code retour `0`

## Résumé des points à retenir

- Le blocage est basé sur `users.locked_at` (et non sur un booléen dédié).
- Le compteur de tentatives est `users.failed_login_attempts`.
- Le seuil se pilote avec `MAX_FAILED_LOGIN_ATTEMPTS` (0 = désactiver).
- Un compte verrouillé bloque immédiatement l’auth (avant la tentative), avec le message `auth.locked`.
- Déblocage manuel possible via `POST /admin/users/unlock` et via `php artisan lnc:unlock-user {email}`.

