andydefer / authentication-kit
Headless authentication kit for Laravel built on Nemesis and Laravel Actions
Requires
- php: ^8.1
- andydefer/directive-forge: ^2.17.16
- andydefer/laravel-nemesis: ^1.15.4
- andydefer/laravel-notification: ^1.5.3
- andydefer/laravel-otp: ^1.1.31
- jenssegers/agent: ^2.6
Requires (Dev)
- laravel/pint: ^1.29
- laravel/tinker: ^2.11
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
This package is auto-updated.
Last update: 2026-08-16 09:47:27 UTC
README
📖 Table des matières
- Introduction
- Installation
- Configuration
- Préparation du modèle
- Routes et API
- API Reference
- Le service d'authentification
- Extension du service
- Actions internes
- Logs et journalisation
- Gestion des erreurs
- Exemples d'utilisation
- Sécurité
- Migration de versions
- Structure du package
🎯 Introduction
Authentication Kit est un package PHP qui fournit un système d'authentification headless, découplé du modèle et prêt à l'emploi.
L'idée principale
Un système d'authentification qui ne connaît pas votre modèle.
Vous pouvez l'utiliser avec n'importe quel modèle Eloquent (User, Shop, CheckPoint, Admin, Client, Partner, etc.) sans avoir à réécrire la logique d'authentification.
Pourquoi ce package ?
| Problème | Solution |
|---|---|
| Authentification liée à un seul modèle | ✅ Multi-modèles supportés |
| Code dupliqué pour chaque modèle | ✅ Service unique et générique |
| Pas de headless API | ✅ API-first, JSON uniquement |
| Dur à intégrer avec React/Kotlin/Swift | ✅ Routes REST standards |
| Pas de logging | ✅ Logging intégré |
| Pas de rate limiting | ✅ Rate limiting configurable |
| Tokens uniquement en Bearer | ✅ Support Bearer + Cookies |
| Pas d'endpoint utilisateur courant | ✅ Route /me intégrée |
🚀 Installation
composer require andydefer/authentication-kit
Laravel
Le package s'enregistre automatiquement via AuthenticationKitServiceProvider.
Publier la configuration
php artisan vendor:publish --tag=authentication-kit-config
Publier les routes
php artisan vendor:publish --tag=authentication-kit-routes
⚙️ Configuration
Fichier de configuration
// config/authentication-kit.php return [ /** * Nom du token d'authentification * Utilisé comme nom du cookie et pour l'identification des tokens */ 'token_name' => env('AUTH_KIT_TOKEN_NAME', 'authentication-kit'), /** * Limite de taux pour la réinitialisation de mot de passe * Nombre de tentatives autorisées par période */ 'password_reset_rate_limit' => env('AUTH_KIT_PASSWORD_RESET_RATE_LIMIT', 3), /** * Limite de taux pour la vérification d'email * Nombre de tentatives autorisées par période */ 'email_verification_rate_limit' => env('AUTH_KIT_EMAIL_VERIFICATION_RATE_LIMIT', 5), /** * Stockage du token dans un cookie * Si true, le token est automatiquement stocké dans un cookie après login/register * Utile pour les applications web avec sessions */ 'store_token_in_cookie' => env('AUTH_KIT_STORE_TOKEN_IN_COOKIE', true), ];
Variables d'environnement
# .env AUTH_KIT_TOKEN_NAME=my_auth_token AUTH_KIT_PASSWORD_RESET_RATE_LIMIT=3 AUTH_KIT_EMAIL_VERIFICATION_RATE_LIMIT=5 AUTH_KIT_STORE_TOKEN_IN_COOKIE=true
Configuration des cookies (Nemesis)
// config/nemesis.php 'web' => [ 'login_route' => '/login', 'dashboard_route' => '/dashboard', 'cookie_name' => 'auth_token', // Nom du cookie 'cookie_secure' => env('COOKIE_SECURE', true), // HTTPS uniquement 'cookie_httponly' => true, // Non accessible en JS 'cookie_samesite' => 'lax', // Protection CSRF ],
🏗️ Préparation du modèle
Votre modèle n'a besoin d'implémenter QUE MailAuthenticatable.
Interface MailAuthenticatable
<?php namespace AndyDefer\AuthenticationKit\Mail\Contracts; use AndyDefer\AuthenticationKit\Contracts\Authenticatable; use AndyDefer\PhpVo\ValueObjects\DateTimeVO; use Illuminate\Database\Eloquent\Model; interface MailAuthenticatable extends Authenticatable { /** * Returns the authentication service instance. */ public static function getMailAuthService(): MailAuthenticationInterface; /** * Gets the email verification timestamp. */ public function getEmailVerifiedAt(): ?DateTimeVO; /** * Creates a new entity from validated data. */ public static function generate(array $data): Model&Authenticatable; /** * Formats the entity for API responses. * Cette méthode est requise par l'interface MustNemesis. */ public function nemesisFormat(): AbstractData; }
Exemple 1 : Modèle User
<?php declare(strict_types=1); namespace App\Models; use AndyDefer\AuthenticationKit\Mail\Contracts\MailAuthenticatable; use AndyDefer\AuthenticationKit\Mail\Contracts\MailAuthenticationInterface; use AndyDefer\AuthenticationKit\Mail\Services\MailAuthenticationService; use AndyDefer\DomainStructures\Abstracts\AbstractData; use AndyDefer\PhpVo\ValueObjects\DateTimeVO; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; /** * User model with authentication capabilities. */ final class User extends Model implements MailAuthenticatable { protected $table = 'users'; protected $fillable = [ 'name', 'email', 'password', 'email_verified_at', ]; protected $hidden = [ 'password', 'remember_token', ]; protected $casts = [ 'email_verified_at' => 'datetime', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; // ============================================================ // MailAuthenticatable - méthodes requises // ============================================================ public static function getMailAuthService(): MailAuthenticationInterface { return MailAuthenticationService::for(self::class); } public function getEmailVerifiedAt(): ?DateTimeVO { if ($this->email_verified_at === null) { return null; } return new DateTimeVO($this->email_verified_at->toIso8601String()); } public static function generate(array $data): Model&MailAuthenticatable { // Validation des champs spécifiques au modèle $validator = Validator::make($data, [ 'name' => ['required', 'string', 'min:2', 'max:255'], ]); if ($validator->fails()) { throw new ValidationException($validator); } return self::create([ 'name' => $data['name'], 'email' => strtolower($data['email']), 'password' => Hash::make($data['password']), ]); } // ============================================================ // MustNemesis - format des données pour l'API // ============================================================ public function nemesisFormat(): AbstractData { return new UserData( id: $this->id, name: $this->name, email: $this->email, emailVerifiedAt: $this->email_verified_at?->toIso8601String(), createdAt: $this->created_at?->toIso8601String(), updatedAt: $this->updated_at?->toIso8601String(), ); } }
Data Object pour la réponse
<?php declare(strict_types=1); namespace App\Models\Data; use AndyDefer\DomainStructures\Abstracts\AbstractData; final class UserData extends AbstractData { public function __construct( public readonly int $id, public readonly string $name, public readonly string $email, public readonly ?string $emailVerifiedAt, public readonly ?string $createdAt, public readonly ?string $updatedAt, ) {} }
Exemple 2 : Modèle Shop (Boutique)
<?php declare(strict_types=1); namespace App\Models; use AndyDefer\AuthenticationKit\Mail\Contracts\MailAuthenticatable; use AndyDefer\AuthenticationKit\Mail\Contracts\MailAuthenticationInterface; use AndyDefer\AuthenticationKit\Mail\Services\MailAuthenticationService; use AndyDefer\DomainStructures\Abstracts\AbstractData; use AndyDefer\PhpVo\ValueObjects\DateTimeVO; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; final class Shop extends Model implements MailAuthenticatable { protected $table = 'shops'; protected $fillable = [ 'name', 'email', 'password', 'email_verified_at', 'owner_name', 'siret', 'phone', 'address', 'is_active', ]; protected $hidden = ['password']; protected $casts = [ 'email_verified_at' => 'datetime', 'is_active' => 'boolean', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; public static function getMailAuthService(): MailAuthenticationInterface { return MailAuthenticationService::for(self::class); } public function getEmailVerifiedAt(): ?DateTimeVO { if ($this->email_verified_at === null) { return null; } return new DateTimeVO($this->email_verified_at->toIso8601String()); } public static function generate(array $data): Model&MailAuthenticatable { $validator = Validator::make($data, [ 'name' => ['required', 'string', 'min:2', 'max:255'], 'owner_name' => ['required', 'string', 'min:2', 'max:255'], 'siret' => ['required', 'string', 'size:14'], 'phone' => ['required', 'string'], ]); if ($validator->fails()) { throw new ValidationException($validator); } return self::create([ 'name' => $data['name'], 'email' => strtolower($data['email']), 'password' => Hash::make($data['password']), 'owner_name' => $data['owner_name'], 'siret' => $data['siret'], 'phone' => $data['phone'], 'address' => $data['address'] ?? null, 'is_active' => $data['is_active'] ?? true, ]); } public function nemesisFormat(): AbstractData { return new ShopData( id: $this->id, name: $this->name, email: $this->email, ownerName: $this->owner_name, siret: $this->siret, phone: $this->phone, isActive: $this->is_active, emailVerifiedAt: $this->email_verified_at?->toIso8601String(), createdAt: $this->created_at?->toIso8601String(), updatedAt: $this->updated_at?->toIso8601String(), ); } }
🗺️ Routes et API
Définition des routes
<?php use AndyDefer\Actions\Http\Requests\EmptyRequest; use AndyDefer\AuthenticationKit\Mail\Actions\EmailLoginAction; use AndyDefer\AuthenticationKit\Mail\Actions\EmailLogoutAction; use AndyDefer\AuthenticationKit\Mail\Actions\EmailRegisterAction; use AndyDefer\AuthenticationKit\Mail\Actions\GetCurrentUserAction; use AndyDefer\AuthenticationKit\Mail\Actions\ResendEmailVerificationAction; use AndyDefer\AuthenticationKit\Mail\Actions\ResetPasswordAction; use AndyDefer\AuthenticationKit\Mail\Actions\SendEmailVerificationAction; use AndyDefer\AuthenticationKit\Mail\Actions\SendPasswordResetLinkAction; use AndyDefer\AuthenticationKit\Mail\Actions\VerifyEmailAction; use AndyDefer\AuthenticationKit\Mail\Requests\EmailLoginRequest; use AndyDefer\AuthenticationKit\Mail\Requests\EmailLogoutRequest; use AndyDefer\AuthenticationKit\Mail\Requests\EmailRegisterRequest; use AndyDefer\AuthenticationKit\Mail\Requests\ResendEmailVerificationRequest; use AndyDefer\AuthenticationKit\Mail\Requests\ResetPasswordRequest; use AndyDefer\AuthenticationKit\Mail\Requests\SendEmailVerificationRequest; use AndyDefer\AuthenticationKit\Mail\Requests\SendPasswordResetLinkRequest; use AndyDefer\AuthenticationKit\Mail\Requests\VerifyEmailRequest; use Illuminate\Support\Facades\Route; /* * Routes publiques - Authentification par email */ Route::middleware(['validate.mail.authenticatable'])->group(function (): void { // Inscription Route::post('/register', action_route( EmailRegisterRequest::class, EmailRegisterAction::class ))->name('register'); // Connexion Route::post('/login', action_route( EmailLoginRequest::class, EmailLoginAction::class ))->name('login'); // Demande de réinitialisation de mot de passe Route::post('/forgot-password', action_route( SendPasswordResetLinkRequest::class, SendPasswordResetLinkAction::class ))->name('password.email'); // Confirmation de réinitialisation Route::post('/reset-password', action_route( ResetPasswordRequest::class, ResetPasswordAction::class ))->name('password.update'); // Vérification d'email Route::post('/email/verify', action_route( VerifyEmailRequest::class, VerifyEmailAction::class ))->name('verification.verify'); /* * Routes protégées - Nécessitent un token d'authentification */ Route::middleware(['nemesis.token'])->group(function (): void { // Déconnexion Route::post('/logout', action_route( EmailLogoutRequest::class, EmailLogoutAction::class ))->name('logout'); // Envoi OTP de vérification email Route::post('/email/verification', action_route( SendEmailVerificationRequest::class, SendEmailVerificationAction::class ))->name('verification.send'); // Renvoi OTP de vérification email Route::post('/email/resend', action_route( ResendEmailVerificationRequest::class, ResendEmailVerificationAction::class ))->name('verification.resend'); }); }); /* * Route de l'utilisateur courant * Supporte à la fois Bearer token et Cookie * Pas de middleware requis - l'action gère elle-même l'authentification */ Route::post('/me', action_route( EmptyRequest::class, GetCurrentUserAction::class ))->name('me');
Tableau récapitulatif des routes
| Route | Méthode | Auth | Support Cookie | Description |
|---|---|---|---|---|
/register |
POST | ❌ | ✅ (si with_token) | Inscription utilisateur |
/login |
POST | ❌ | ✅ (si configuré) | Connexion |
/logout |
POST | ✅ | ✅ (supprime cookie) | Déconnexion |
/me |
POST | ✅ | ✅ | Utilisateur courant |
/forgot-password |
POST | ❌ | ❌ | Demande reset OTP |
/reset-password |
POST | ❌ | ❌ | Réinitialisation |
/email/verify |
POST | ❌ | ❌ | Vérification email |
/email/verification |
POST | ✅ | ❌ | Envoi OTP vérification |
/email/resend |
POST | ✅ | ❌ | Renvoi OTP vérification |
🍪 Support des cookies
Le package supporte le stockage automatique des tokens d'authentification dans les cookies, rendant l'intégration avec les applications web plus fluide.
Configuration
// config/authentication-kit.php 'store_token_in_cookie' => env('AUTH_KIT_STORE_TOKEN_IN_COOKIE', true),
Comment ça fonctionne
| Événement | Comportement |
|---|---|
| Connexion | Le token est automatiquement stocké dans un cookie sécurisé |
| Inscription avec token | Le cookie est défini si with_token = true |
| Déconnexion | Le cookie est automatiquement supprimé |
Requête /me |
Le token est lu depuis le cookie si pas de Bearer token |
Priorité d'authentification
- Bearer token (header
Authorization) - Prioritaire - Cookie token - Utilisé si aucun Bearer token n'est présent
// Exemple : Priorité Bearer > Cookie // Headers: Authorization: Bearer token-api // Cookie: auth_token=token-cookie // → Utilise le Bearer token
Configuration des cookies
// config/nemesis.php 'web' => [ 'login_route' => '/login', 'dashboard_route' => '/dashboard', 'cookie_name' => 'auth_token', // Nom du cookie 'cookie_secure' => env('COOKIE_SECURE', true), // HTTPS uniquement 'cookie_httponly' => true, // Non accessible en JS 'cookie_samesite' => 'lax', // Protection CSRF ],
📋 API Reference
1. Inscription - POST /register
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
with_token |
boolean |
❌ Non | Générer un token (défaut: false) |
* |
mixed |
❌ Non | Tous les autres champs sont passés au modèle |
Requête :
{
"model_type": "App\\Models\\User",
"with_token": true,
"name": "John Doe",
"email": "john@example.com",
"password": "Password123!",
"password_confirmation": "Password123!"
}
Réponse (201 Created) :
{
"message": "Registration successful",
"auth": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"emailVerifiedAt": null,
"createdAt": "2026-08-14T10:00:00+00:00",
"updatedAt": "2026-08-14T10:00:00+00:00"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Erreur (422) :
{
"message": "Validation error",
"status": 422,
"errorCode": "VALIDATION_ERROR",
"errors": {
"email": ["The email has already been taken."]
}
}
2. Connexion - POST /login
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
email |
string |
✅ Oui | Email de l'utilisateur |
password |
string |
✅ Oui | Mot de passe |
Requête :
{
"model_type": "App\\Models\\User",
"email": "john@example.com",
"password": "Password123!"
}
Réponse (200 OK) :
{
"message": "Login successful",
"auth": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"emailVerifiedAt": "2026-08-14T12:00:00+00:00",
"createdAt": "2026-08-14T10:00:00+00:00",
"updatedAt": "2026-08-14T12:00:00+00:00"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Erreur - Credentials manquants (400) :
{
"message": "Email and password are required",
"status": 400,
"errorCode": "MISSING_CREDENTIALS",
"errors": {
"email": ["The email field is required."],
"password": ["The password field is required."]
}
}
Erreur - Identifiants invalides (401) :
{
"message": "Invalid credentials",
"status": 401,
"errorCode": "INVALID_CREDENTIALS"
}
3. Utilisateur courant - POST /me
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
❌ Non | Optionnel, non utilisé par l'action |
Requête avec Bearer Token :
POST /me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Requête avec Cookie :
POST /me
Cookie: auth_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Réponse (200 OK) :
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"emailVerifiedAt": "2026-08-14T12:00:00+00:00",
"createdAt": "2026-08-14T10:00:00+00:00",
"updatedAt": "2026-08-14T12:00:00+00:00"
}
Erreur - Non authentifié (401) :
{
"message": "Unauthenticated",
"status": 401,
"errorCode": "UNAUTHENTICATED"
}
4. Déconnexion - POST /logout
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
token |
string |
✅ Oui | Token à révoquer |
Requête :
{
"model_type": "App\\Models\\User",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Réponse (204 No Content)
Erreur - Token invalide (401) :
{
"message": "Invalid token",
"status": 401,
"errorCode": "INVALID_TOKEN"
}
Erreur - Token expiré (401) :
{
"message": "Token has expired",
"status": 401,
"errorCode": "TOKEN_EXPIRED"
}
5. Demande réinitialisation - POST /forgot-password
| Champ | Type | Requis | Description |
|---|---|---|---|
email |
string |
✅ Oui | Email de l'utilisateur |
model_type |
string |
✅ Oui | FQCN du modèle |
Requête :
{
"email": "john@example.com",
"model_type": "App\\Models\\User"
}
Réponse (200 OK) :
{
"message": "Password reset OTP sent successfully",
"email": "john@example.com",
"sentAt": "2026-08-14T12:00:00+00:00"
}
🔒 Sécurité : Retourne toujours 200, que l'utilisateur existe ou non.
6. Réinitialisation - POST /reset-password
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
email |
string |
✅ Oui | Email de l'utilisateur |
token |
string |
✅ Oui | Code OTP |
password |
string |
✅ Oui | Nouveau mot de passe |
password_confirmation |
string |
✅ Oui | Confirmation |
Requête :
{
"model_type": "App\\Models\\User",
"email": "john@example.com",
"token": "123456",
"password": "NewPassword123!",
"password_confirmation": "NewPassword123!"
}
Réponse (200 OK) :
{
"message": "Password reset successfully",
"email": "john@example.com",
"resetAt": "2026-08-14T12:00:00+00:00"
}
Erreur - OTP invalide (400) :
{
"message": "Invalid or expired reset OTP",
"status": 400,
"errorCode": "INVALID_RESET_OTP"
}
7. Vérification email - POST /email/verify
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
email |
string |
✅ Oui | Email de l'utilisateur |
token |
string |
✅ Oui | Code OTP |
Requête :
{
"model_type": "App\\Models\\User",
"email": "john@example.com",
"token": "123456"
}
Réponse (200 OK) :
{
"message": "Email verified successfully",
"email": "john@example.com",
"verifiedAt": "2026-08-14T12:00:00+00:00",
"alreadyVerified": false
}
Réponse - Déjà vérifié :
{
"message": "Email already verified",
"email": "john@example.com",
"verifiedAt": "2026-08-14T10:00:00+00:00",
"alreadyVerified": true
}
8. Envoi OTP vérification - POST /email/verification
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
auth_id |
`integer | string` | ✅ Oui |
Requête :
{
"model_type": "App\\Models\\User",
"auth_id": 1
}
Réponse (200 OK) :
{
"message": "Verification OTP sent successfully",
"email": "john@example.com",
"sentAt": "2026-08-14T12:00:00+00:00"
}
9. Renvoi OTP vérification - POST /email/resend
| Champ | Type | Requis | Description |
|---|---|---|---|
model_type |
string |
✅ Oui | FQCN du modèle |
auth_id |
`integer | string` | ✅ Oui |
Requête :
{
"model_type": "App\\Models\\User",
"auth_id": 1
}
Réponse (200 OK) :
{
"message": "Verification OTP resent successfully",
"email": "john@example.com",
"sentAt": "2026-08-14T12:00:00+00:00"
}
🧩 Le service d'authentification
Qu'est-ce que MailAuthenticationService ?
C'est un service générique qui orchestre toute la logique d'authentification :
$authService = MailAuthenticationService::for(User::class);
Méthodes publiques
| Méthode | Description |
|---|---|
register(AbstractRecord $record) |
Crée un nouvel utilisateur |
login(string $email, string $password) |
Authentifie un utilisateur |
logout(Authenticatable&Model $user, string $token) |
Révoque un token |
sendPasswordResetOtp(string $email) |
Envoie un OTP de réinitialisation |
resetPassword(string $email, string $code, string $password) |
Réinitialise le mot de passe |
sendEmailVerificationOtp(Authenticatable $user) |
Envoie un OTP de vérification |
verifyEmail(string $email, string $code) |
Vérifie l'email |
resendEmailVerificationOtp(Authenticatable $user) |
Renvoie un OTP de vérification |
isEmailVerified(Authenticatable $user) |
Vérifie si l'email est vérifié |
userExists(string $email) |
Vérifie l'existence d'un utilisateur |
Hooks extensibles
| Hook | Quand | Cas d'usage |
|---|---|---|
beforeRegister() |
Avant inscription | IP check, anti-spam |
afterRegister() |
Après inscription | Email bienvenue, création profil |
beforeLogin() |
Avant connexion | Compte bloqué, 2FA |
afterLogin() |
Après connexion | Last login, sessions |
beforeLogout() |
Avant déconnexion | Journalisation |
afterLogout() |
Après déconnexion | Nettoyage sessions |
beforeSendPasswordResetOtp() |
Avant OTP reset | Vérification email |
afterSendPasswordResetOtp() |
Après OTP reset | Notification admin |
beforeResetPassword() |
Avant reset | Validation supplémentaire |
afterResetPassword() |
Après reset | Invalidation sessions |
beforeVerifyEmail() |
Avant vérif email | Vérifications supplémentaires |
afterVerifyEmail() |
Après vérif email | Activation compte |
Template Method Pattern - Personnalisation des notifications
/** * Build the password reset notification message. * Override this method to customize the password reset email. */ protected function buildPasswordResetNotification(string $email, string $otp): NotificationMessageRecord { return NotificationMessageRecord::from([ 'email' => $email, 'subject' => 'Password Reset Code', 'body' => "Your password reset code is: {$otp}", ]); } /** * Build the email verification notification message. * Override this method to customize the email verification email. */ protected function buildEmailVerificationNotification(string $email, string $otp): NotificationMessageRecord { return NotificationMessageRecord::from([ 'email' => $email, 'subject' => 'Email Verification Code', 'body' => "Your email verification code is: {$otp}", ]); }
🔧 Extension du service
Exemple : Service personnalisé complet
<?php declare(strict_types=1); namespace App\Services; use AndyDefer\AuthenticationKit\Mail\Services\MailAuthenticationService; use AndyDefer\AuthenticationKit\Contracts\Authenticatable; use AndyDefer\AuthenticationKit\Mail\Records\NotificationMessageRecord; use AndyDefer\DomainStructures\Abstracts\AbstractRecord; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; /** * Service d'authentification personnalisé avec des hooks métier. */ final class CustomAuthService extends MailAuthenticationService { // ============================================================ // TEMPLATE METHOD - Personnalisation des notifications // ============================================================ protected function buildPasswordResetNotification(string $email, string $otp): NotificationMessageRecord { $html = view('emails.password-reset', [ 'email' => $email, 'otp' => $otp, 'expires_in' => 10, ])->render(); return NotificationMessageRecord::from([ 'email' => $email, 'subject' => '🔐 Réinitialisation de votre mot de passe - Afya Medical', 'body' => $html, ]); } protected function buildEmailVerificationNotification(string $email, string $otp): NotificationMessageRecord { $html = view('emails.verify-email', [ 'email' => $email, 'otp' => $otp, 'expires_in' => 5, ])->render(); return NotificationMessageRecord::from([ 'email' => $email, 'subject' => '📧 Vérification de votre email - Afya Medical', 'body' => $html, ]); } // ============================================================ // HOOKS - Logique métier personnalisée // ============================================================ protected function beforeRegister(AbstractRecord $record): void { // Vérifier si l'IP est bloquée if ($this->isIpBlocked($record->ip)) { throw new \RuntimeException('IP blocked due to suspicious activity'); } // Vérifier si l'email est dans une liste noire $email = $record->data->get('email'); if ($this->isEmailBlocked($email)) { throw new \RuntimeException('This email address is not allowed'); } } protected function afterRegister(Model&Authenticatable $user, AbstractRecord $record): void { // 1. Envoyer un email de bienvenue $this->sendWelcomeEmail($user); // 2. Créer un profil utilisateur $user->profile()->create([ 'bio' => $record->data->get('bio'), 'age' => $record->data->get('age'), 'phone' => $record->data->get('phone'), ]); // 3. Attribuer un rôle par défaut $user->assignRole('user'); // 4. Logger Log::info('New user registered', ['user_id' => $user->id]); } protected function beforeLogin(string $email, string $password): void { // Vérifier si le compte est verrouillé $user = $this->findUserByEmail($email); if ($user && $user->is_locked) { throw new \RuntimeException('Account is locked. Please contact support.'); } // Vérifier si l'IP est autorisée if (! $this->isIpAllowed(request()->ip())) { throw new \RuntimeException('Access denied from this IP address'); } } protected function afterLogin(Model&Authenticatable $user): void { // 1. Mettre à jour la dernière connexion $user->last_login_at = now(); $user->login_count = ($user->login_count ?? 0) + 1; $user->save(); // 2. Enregistrer la session $this->createUserSession($user); // 3. Nettoyer les tentatives échouées $this->clearFailedAttempts($user); // 4. Logger Log::info('User logged in', ['user_id' => $user->id]); } protected function beforeLogout(Authenticatable&Model $authenticatable, string $plainToken): void { Log::info('Logout attempt', ['user_id' => $authenticatable->id]); } protected function afterLogout(Authenticatable&Model $authenticatable): void { // 1. Supprimer la session $this->clearUserSession($authenticatable); // 2. Journaliser Log::info('Logout successful', ['user_id' => $authenticatable->id]); } protected function beforeSendPasswordResetOtp(string $email): void { $user = $this->findUserByEmail($email); if ($user && $user->prevent_password_reset) { throw new \RuntimeException('Password reset is not allowed for this account'); } } protected function afterSendPasswordResetOtp(string $email, bool $success): void { if (! $success) { $this->notifyAdmin('Password reset failed for: ' . $email); } } protected function beforeResetPassword(string $email, string $code, string $password): void { // Valider que le mot de passe est assez fort if (strlen($password) < 12) { throw new \RuntimeException('Password must be at least 12 characters'); } // Vérifier que le mot de passe n'est pas compromis if ($this->isPasswordCompromised($password)) { throw new \RuntimeException('This password has been compromised. Please choose another.'); } } protected function afterResetPassword(Model&Authenticatable $user): void { // 1. Invalider toutes les sessions $user->tokens()->delete(); // 2. Notifier l'utilisateur $this->sendPasswordChangedNotification($user); // 3. Logger Log::alert('Password reset', ['user_id' => $user->id]); } protected function beforeVerifyEmail(string $email, string $code): void { $user = $this->findUserByEmail($email); if ($user && $user->email_verified_at !== null) { throw new \RuntimeException('Email already verified'); } } protected function afterVerifyEmail(Model&Authenticatable $user): void { // 1. Activer le compte $user->is_active = true; $user->save(); // 2. Envoyer une notification $this->sendWelcomeVerificationNotification($user); // 3. Logger Log::info('Email verified', ['user_id' => $user->id]); } // ============================================================ // MÉTHODES PRIVÉES UTILITAIRES // ============================================================ private function findUserByEmail(string $email): ?Model { /** @var class-string<Model> $modelClass */ $modelClass = $this->modelClass; return $modelClass::where('email', $email)->first(); } private function isIpBlocked(?string $ip): bool { $blockedIps = ['192.168.1.1', '10.0.0.1']; return in_array($ip, $blockedIps); } private function isEmailBlocked(?string $email): bool { $blockedDomains = ['spam.com', 'blocked.com']; $domain = substr(strrchr($email, '@'), 1); return in_array($domain, $blockedDomains); } private function isIpAllowed(string $ip): bool { return true; } private function isPasswordCompromised(string $password): bool { return false; } private function sendWelcomeEmail(Model&Authenticatable $user): void {} private function sendWelcomeVerificationNotification(Model&Authenticatable $user): void {} private function sendPasswordChangedNotification(Model&Authenticatable $user): void {} private function notifyAdmin(string $message): void {} private function createUserSession(Model&Authenticatable $user): void {} private function clearUserSession(Model&Authenticatable $user): void {} private function clearFailedAttempts(Model&Authenticatable $user): void {} }
Template Method Pattern - Personnalisation de la validation du mot de passe
/** * Get the password validation rules. * Override this method to customize password validation. * * @return array<string, array<int, mixed>> */ public static function getPasswordValidationRules(): array { return [ 'password' => ['required', 'string', 'min:8', 'confirmed'], ]; }
Exemple de personnalisation :
// Dans votre service personnalisé public static function getPasswordValidationRules(): array { return [ 'password' => [ 'required', 'string', 'min:12', 'confirmed', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[@$!%*?&]/', 'not_in:password,123456,admin', ], ]; }
⚙️ Actions internes
Le package utilise le pattern Action pour organiser la logique métier de manière modulaire et testable.
Structure d'une Action
abstract class AbstractAction { // Préparation - validation des données protected function before(AbstractRecord $record): void {} // Traitement principal protected function handle(AbstractRecord $record): ResponseFactory {} // Nettoyage et journalisation protected function after(bool $success, ?Exception $error, AbstractRecord $record): void {} }
Actions disponibles
| Action | Description | Record | Auth |
|---|---|---|---|
EmailRegisterAction |
Inscription utilisateur | EmailRegisterAuthRecord |
❌ |
EmailLoginAction |
Connexion utilisateur | EmailLoginAuthRecord |
❌ |
EmailLogoutAction |
Déconnexion utilisateur | EmailLogoutAuthRecord |
✅ |
GetCurrentUserAction |
Récupération utilisateur courant | EmptyRequest |
✅ |
SendPasswordResetLinkAction |
Envoi OTP de réinitialisation | SendPasswordResetLinkRecord |
❌ |
ResetPasswordAction |
Réinitialisation mot de passe | ResetPasswordRecord |
❌ |
SendEmailVerificationAction |
Envoi OTP de vérification | SendEmailVerificationRecord |
✅ |
ResendEmailVerificationAction |
Renvoi OTP de vérification | ResendEmailVerificationRecord |
✅ |
VerifyEmailAction |
Vérification email | VerifyEmailRecord |
❌ |
Flux d'exécution d'une Action
Requête entrante (Record)
↓
1. before() - Validation et préparation
↓
2. handle() - Traitement principal
├── Succès → Réponse positive
└── Échec → ErrorResponseData
↓
3. after() - Journalisation
├── Succès → logSuccess()
└── Échec → logFailure()
Exemple : EmailLoginAction
final class EmailLoginAction extends AbstractAction { protected function before(AbstractRecord $record): void { // Extrait les données du record $this->modelClass = $record->model_type; $this->ip = $record->ip; $this->userAgent = $record->user_agent; } protected function handle(AbstractRecord $record): ResponseFactory { // 1. Validation des identifiants $email = $record->data->get('email'); $password = $record->data->get('password'); if ($email === null || $password === null) { return $this->errorResponse(ErrorCode::MISSING_CREDENTIALS); } // 2. Tentative de connexion via le service $service = $this->modelClass::getMailAuthService(); $token = $service->login($email, $password); if ($token === null) { return $this->errorResponse(ErrorCode::INVALID_CREDENTIALS); } // 3. Création du token via Nemesis [$tokenModel, $plainToken] = $this->nemesis->createWithPlainToken( new NemesisTokenRecord(...), $authenticatable ); // 4. Réponse de succès return ResponseFactory::json(new AuthLoginData(...), 200); } protected function after(bool $success, ?Exception $error, AbstractRecord $record): void { if ($this->success) { $this->logRepository->loginSuccess(...); } else { $this->logRepository->loginFailure(...); } } }
Middleware associé
Le package fournit un middleware qui valide automatiquement le champ model_type :
// ValidateMailAuthenticatableMiddleware public function handle(Request $request, Closure $next): Response { $modelType = $request->input('model_type'); // 1. Validation de la présence if ($modelType === null) { return $this->errorResponse('MODEL_TYPE_REQUIRED', 400); } // 2. Validation de l'existence if (! class_exists($modelType)) { return $this->errorResponse('MODEL_NOT_FOUND', 500); } // 3. Validation de l'interface if (! in_array(MailAuthenticatable::class, class_implements($modelType))) { return $this->errorResponse('INVALID_MODEL', 500); } // 4. Liaison du service app()->bind(MailAuthenticationInterface::class, function () use ($modelType) { return MailAuthenticationService::for($modelType); }); // 5. Exécution de la requête $response = $next($request); // 6. Ajout des cookies si configuré if ($this->config->shouldStoreTokenInCookie()) { foreach (Cookie::getQueuedCookies() as $cookie) { $response->headers->setCookie($cookie); } } return $response; }
📊 Logs et journalisation
Événements journalisés
| Événement | Méthode | Données |
|---|---|---|
| Inscription réussie | logRegistrationSuccess() |
authId, modelClass, withToken |
| Inscription échouée | logRegistrationFailure() |
modelClass, error, errorType |
| Connexion réussie | loginSuccess() |
authId, modelClass, email |
| Connexion échouée | loginFailure() |
modelClass, email, error, errorType |
| Déconnexion réussie | logoutSuccess() |
authId, modelClass, email |
| Déconnexion échouée | logoutFailure() |
modelClass, email, error, errorType |
| Reset envoyé | logPasswordResetLinkSent() |
email, success, error |
| Reset échoué | logPasswordResetFailure() |
email, error, errorType |
| Vérification réussie | logVerificationSuccess() |
email, modelClass, alreadyVerified |
| Vérification échouée | logVerificationFailure() |
email, modelClass, error, errorType |
Structure des logs
{
"event": "user_login_success",
"auth_id": 1,
"model_type": "App\\Models\\User",
"email": "john@example.com",
"timestamp": "2026-08-14T10:00:00+00:00",
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0 ..."
}
Implémentation personnalisée du LogRepository
<?php use AndyDefer\AuthenticationKit\Mail\Contracts\Repositories\LogRepositoryInterface; use AndyDefer\AuthenticationKit\Enums\ErrorType; class CustomLogRepository implements LogRepositoryInterface { public function logRegistrationSuccess( int $authId, string $modelClass, bool $withToken, ): void { \Log::info('User registered', [ 'auth_id' => $authId, 'model_class' => $modelClass, 'with_token' => $withToken, ]); } public function logRegistrationFailure( string $modelClass, string $error, ErrorType $errorType, ): void { \Log::warning('Registration failed', [ 'model_class' => $modelClass, 'error' => $error, 'error_type' => $errorType->value, ]); } public function loginFailure( string $modelClass, string $email, string $error, ErrorType $errorType, ): void { \Log::warning('Login failed', [ 'model_class' => $modelClass, 'email' => $email, 'error' => $error, 'error_type' => $errorType->value, ]); } // ... autres méthodes }
🏷️ Gestion des erreurs
ErrorCode (Réponses API)
Le package utilise l'énumération ErrorCode pour standardiser les réponses d'erreur.
| Code | HTTP | Description |
|---|---|---|
INVALID_RECORD_TYPE |
500 | Type de record invalide |
MISSING_CREDENTIALS |
400 | Identifiants manquants |
INVALID_CREDENTIALS |
401 | Identifiants invalides |
AUTHENTICATABLE_NOT_FOUND |
401 | Utilisateur non trouvé |
INVALID_TOKEN |
401 | Token invalide |
TOKEN_EXPIRED |
401 | Token expiré |
VALIDATION_ERROR |
422 | Erreur de validation |
MODEL_NOT_FOUND |
500 | Modèle introuvable |
INVALID_MODEL |
500 | Modèle invalide |
REGISTRATION_ERROR |
500 | Erreur d'inscription |
LOGIN_ERROR |
500 | Erreur de connexion |
LOGOUT_FAILED |
500 | Échec de déconnexion |
LOGOUT_EXCEPTION |
500 | Exception lors de la déconnexion |
USER_FETCH_ERROR |
500 | Erreur de récupération utilisateur |
VERIFICATION_OTP_RESEND_FAILED |
500 | Échec renvoi OTP |
INVALID_RESET_OTP |
400 | OTP de réinitialisation invalide |
RESET_PASSWORD_ERROR |
500 | Erreur de réinitialisation |
ErrorType (Logs)
L'énumération ErrorType est utilisée pour les logs, offrant une meilleure analyse.
| Type | Description |
|---|---|
user_not_found |
Utilisateur non trouvé |
invalid_credentials |
Identifiants invalides |
invalid_otp |
OTP invalide |
rate_limit_exceeded |
Limite de taux dépassée |
token_not_found |
Token non trouvé |
token_revoke_failed |
Échec de révocation |
validation_error |
Erreur de validation |
account_locked |
Compte verrouillé |
email_already_verified |
Email déjà vérifié |
invalid_email |
Email invalide |
password_too_weak |
Mot de passe trop faible |
invalid_token |
Token invalide |
token_expired |
Token expiré |
invalid_record_type |
Type de record invalide |
missing_credentials |
Identifiants manquants |
Structure des erreurs
{
"message": "Email and password are required",
"status": 400,
"errorCode": "MISSING_CREDENTIALS",
"errors": {
"email": ["The email field is required."],
"password": ["The password field is required."]
}
}
🔒 Sécurité
| Fonctionnalité | Description | Valeur par défaut |
|---|---|---|
| Rate Limiting | Nombre de tentatives par période | 3 (reset) / 5 (vérification) |
| OTP Expiration | Durée de validité d'un OTP | 5 min (email) / 10 min (password) |
| OTP Max Attempts | Nombre de tentatives par OTP | 3 |
| Token Hash | Algorithme de hachage des tokens | SHA-256 |
| Réponse /forgot-password | Ne révèle pas l'existence de l'utilisateur | Toujours 200 |
| Logs | Protection des données sensibles | Pas de logs pour emails inexistants |
| Cookies | HttpOnly, Secure, SameSite | Configurables |
| Token Storage | Hashé en base, jamais stocké en clair | SHA-256 |
🧪 Tests
Configuration des tests
// tests/TestCase.php use AndyDefer\AuthenticationKit\Tests\IntegrationTestCase; class YourTest extends IntegrationTestCase { protected function setUp(): void { parent::setUp(); // Configuration pour les tests $this->app['config']->set('authentication-kit.store_token_in_cookie', false); $this->app['config']->set('nemesis.web.cookie_name', 'nemesis_token'); } }
Exemple de test complet
<?php use AndyDefer\AuthenticationKit\Tests\Mail\Fixtures\Models\TestUserMail; final class LoginTest extends IntegrationTestCase { public function test_login_success(): void { // 1. Créer un utilisateur de test $user = TestUserMail::create([ 'name' => 'Test User', 'email' => 'test@example.com', 'password' => bcrypt('Password123!'), ]); // 2. Requête de connexion $payload = [ 'model_type' => TestUserMail::class, 'email' => 'test@example.com', 'password' => 'Password123!', ]; $response = $this->postJson('/api/login', $payload); // 3. Assertions $response->assertStatus(200); $response->assertJsonStructure([ 'message', 'auth' => ['id', 'name', 'email'], 'token', ]); } public function test_login_with_cookie(): void { // 1. Configuration cookie $this->app['config']->set('authentication-kit.store_token_in_cookie', true); $this->refreshConfigService(); // 2. Créer un utilisateur $user = TestUserMail::create([ 'name' => 'Cookie User', 'email' => 'cookie@example.com', 'password' => bcrypt('Password123!'), ]); // 3. Requête de connexion $payload = [ 'model_type' => TestUserMail::class, 'email' => 'cookie@example.com', 'password' => 'Password123!', ]; $response = $this->postJson('/api/login', $payload); // 4. Vérifier le cookie $response->assertStatus(200); $response->assertCookie('nemesis_token'); } public function test_register_with_token(): void { $payload = [ 'model_type' => TestUserMail::class, 'with_token' => true, 'name' => 'Test User', 'email' => 'test@example.com', 'password' => 'Password123!', 'password_confirmation' => 'Password123!', ]; $response = $this->postJson('/api/register', $payload); $response->assertStatus(201); $response->assertJsonStructure(['token', 'auth']); } public function test_me_endpoint(): void { // 1. Login pour obtenir un token $loginPayload = [ 'model_type' => TestUserMail::class, 'email' => 'test@example.com', 'password' => 'Password123!', ]; $loginResponse = $this->postJson('/api/login', $loginPayload); $token = $loginResponse->json('token'); // 2. Requête /me $response = $this->postJson('/api/me', [ 'model_type' => TestUserMail::class, ], [ 'Authorization' => 'Bearer '.$token, ]); $response->assertStatus(200); $response->assertJson(['email' => 'test@example.com']); } public function test_me_endpoint_with_cookie(): void { // 1. Configuration et login avec cookie $this->app['config']->set('authentication-kit.store_token_in_cookie', true); $this->refreshConfigService(); $loginPayload = [ 'model_type' => TestUserMail::class, 'email' => 'test@example.com', 'password' => 'Password123!', ]; $loginResponse = $this->postJson('/api/login', $loginPayload); $cookieValue = $this->getCookieValue($loginResponse, 'nemesis_token'); // 2. Requête /me avec cookie $response = $this->call('POST', '/api/me', [ 'model_type' => TestUserMail::class, ], [ 'nemesis_token' => $cookieValue, ]); $response->assertStatus(200); $response->assertJson(['email' => 'test@example.com']); } }
🔄 Migration de versions
De v1.11.0 à v1.13.1
1. Renommage du middleware
Avant :
use AndyDefer\AuthenticationKit\Mail\Http\Middleware\ValidateMailAuthenticatable;
Après :
use AndyDefer\AuthenticationKit\Mail\Http\Middleware\ValidateMailAuthenticatableMiddleware;
2. Interface LogRepositoryInterface
Avant :
public function loginFailure( string $modelClass, string $email, string $error, string $errorClass, ): void;
Après :
public function loginFailure( string $modelClass, string $email, string $error, ErrorType $errorType, ): void;
3. Nouvelle configuration
Ajouter à config/authentication-kit.php :
'store_token_in_cookie' => env('AUTH_KIT_STORE_TOKEN_IN_COOKIE', true),
4. Nouvelle interface MustNemesis
Le modèle doit maintenant implémenter nemesisFormat() :
public function nemesisFormat(): AbstractData { return new YourData( id: $this->id, email: $this->email, // ... ); }
5. Nouvelle route
Ajouter la route /me dans vos routes :
Route::post('/me', action_route( EmptyRequest::class, GetCurrentUserAction::class ))->name('me');
📦 Structure du package
src/
├── Mail/
│ ├── Actions/
│ │ ├── EmailLoginAction.php # Connexion
│ │ ├── EmailLogoutAction.php # Déconnexion
│ │ ├── EmailRegisterAction.php # Inscription
│ │ ├── GetCurrentUserAction.php # Utilisateur courant
│ │ ├── ResetPasswordAction.php # Réinitialisation
│ │ ├── SendPasswordResetLinkAction.php # Envoi OTP reset
│ │ ├── SendEmailVerificationAction.php # Envoi OTP vérif
│ │ ├── ResendEmailVerificationAction.php # Renvoi OTP
│ │ └── VerifyEmailAction.php # Vérification email
│ ├── Contracts/
│ │ ├── MailAuthenticatable.php # Interface du modèle
│ │ ├── MailAuthenticationInterface.php # Interface du service
│ │ └── Repositories/
│ │ └── LogRepositoryInterface.php # Interface des logs
│ ├── Services/
│ │ └── MailAuthenticationService.php # Service principal
│ ├── Repositories/
│ │ └── LogRepository.php # Implémentation des logs
│ ├── Http/
│ │ └── Middleware/
│ │ └── ValidateMailAuthenticatableMiddleware.php
│ ├── Records/
│ │ ├── EmailLoginAuthRecord.php
│ │ ├── EmailLogoutAuthRecord.php
│ │ └── EmailRegisterAuthRecord.php
│ ├── Datas/
│ │ ├── AuthLoginData.php
│ │ ├── AuthRegisteredData.php
│ │ └── ErrorResponseData.php
│ ├── Enums/
│ │ ├── ErrorCode.php # Codes d'erreur API
│ │ ├── ErrorType.php # Types d'erreur logs
│ │ └── TokenSource.php # Source des tokens
│ └── routes.php # Définition des routes
├── Configs/
│ └── AuthenticationKitConfig.php # Configuration
└── AuthenticationKitServiceProvider.php # Service Provider
📱 Exemples d'utilisation
Exemple 1 : Laravel HTTP Client
<?php declare(strict_types=1); use Illuminate\Support\Facades\Http; class AuthService { private const BASE_URL = 'http://localhost/api'; private const MODEL_TYPE = 'App\\Models\\User'; public function register(array $data): array { $response = Http::post(self::BASE_URL . '/register', [ 'model_type' => self::MODEL_TYPE, ...$data, ]); return $response->json(); } public function login(string $email, string $password): array { $response = Http::post(self::BASE_URL . '/login', [ 'model_type' => self::MODEL_TYPE, 'email' => $email, 'password' => $password, ]); $data = $response->json(); if ($response->successful()) { session(['auth_token' => $data['token']]); } return $data; } public function me(): array { $token = session('auth_token'); $response = Http::withToken($token)->post(self::BASE_URL . '/me', [ 'model_type' => self::MODEL_TYPE, ]); return $response->json(); } public function logout(): void { $token = session('auth_token'); Http::withToken($token)->post(self::BASE_URL . '/logout', [ 'model_type' => self::MODEL_TYPE, 'token' => $token, ]); session()->forget('auth_token'); } public function forgotPassword(string $email): array { $response = Http::post(self::BASE_URL . '/forgot-password', [ 'email' => $email, 'model_type' => self::MODEL_TYPE, ]); return $response->json(); } public function resetPassword(string $email, string $token, string $password): array { $response = Http::post(self::BASE_URL . '/reset-password', [ 'model_type' => self::MODEL_TYPE, 'email' => $email, 'token' => $token, 'password' => $password, 'password_confirmation' => $password, ]); return $response->json(); } public function sendVerification(int $userId): array { $token = session('auth_token'); $response = Http::withToken($token)->post( self::BASE_URL . '/email/verification', [ 'model_type' => self::MODEL_TYPE, 'auth_id' => $userId, ] ); return $response->json(); } public function verifyEmail(string $email, string $token): array { $response = Http::post(self::BASE_URL . '/email/verify', [ 'model_type' => self::MODEL_TYPE, 'email' => $email, 'token' => $token, ]); return $response->json(); } }
Exemple 2 : React / TypeScript
// services/auth.service.ts const API_URL = 'http://localhost/api'; const MODEL_TYPE = 'App\\Models\\User'; interface LoginResponse { message: string; auth: { id: number; name: string; email: string; emailVerifiedAt: string | null; createdAt: string; updatedAt: string; }; token: string; } interface ErrorResponse { message: string; status: number; errorCode: string; errors?: Record<string, string[]>; } class AuthService { private token: string | null = null; constructor() { this.token = localStorage.getItem('auth_token'); } async register(data: { name: string; email: string; password: string; password_confirmation: string; }): Promise<{ success: boolean; data?: any; error?: ErrorResponse }> { try { const response = await fetch(`${API_URL}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: MODEL_TYPE, with_token: true, ...data, }), }); const result = await response.json(); if (response.ok) { if (result.token) { localStorage.setItem('auth_token', result.token); this.token = result.token; } return { success: true, data: result }; } return { success: false, error: result }; } catch (error) { return { success: false, error: error as ErrorResponse }; } } async login(email: string, password: string): Promise<{ success: boolean; data?: LoginResponse; error?: ErrorResponse }> { try { const response = await fetch(`${API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: MODEL_TYPE, email, password, }), }); const result = await response.json(); if (response.ok) { localStorage.setItem('auth_token', result.token); localStorage.setItem('user', JSON.stringify(result.auth)); this.token = result.token; return { success: true, data: result }; } return { success: false, error: result }; } catch (error) { return { success: false, error: error as ErrorResponse }; } } async me(): Promise<{ success: boolean; data?: any; error?: ErrorResponse }> { try { const response = await fetch(`${API_URL}/me`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}`, }, body: JSON.stringify({ model_type: MODEL_TYPE, }), }); const result = await response.json(); if (response.ok) { return { success: true, data: result }; } return { success: false, error: result }; } catch (error) { return { success: false, error: error as ErrorResponse }; } } async logout(): Promise<void> { if (!this.token) return; try { await fetch(`${API_URL}/logout`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}`, }, body: JSON.stringify({ model_type: MODEL_TYPE, token: this.token, }), }); } finally { localStorage.removeItem('auth_token'); localStorage.removeItem('user'); this.token = null; } } async forgotPassword(email: string): Promise<{ success: boolean; data?: any; error?: ErrorResponse }> { try { const response = await fetch(`${API_URL}/forgot-password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, model_type: MODEL_TYPE, }), }); const result = await response.json(); return { success: response.ok, data: result }; } catch (error) { return { success: false, error: error as ErrorResponse }; } } async resetPassword( email: string, token: string, password: string ): Promise<{ success: boolean; data?: any; error?: ErrorResponse }> { try { const response = await fetch(`${API_URL}/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_type: MODEL_TYPE, email, token, password, password_confirmation: password, }), }); const result = await response.json(); return { success: response.ok, data: result }; } catch (error) { return { success: false, error: error as ErrorResponse }; } } getToken(): string | null { return this.token; } isAuthenticated(): boolean { return !!this.token; } } export const authService = new AuthService();
Composant Login React
// components/Login.tsx import React, { useState } from 'react'; import { authService } from '../services/auth.service'; interface LoginFormData { email: string; password: string; } const Login: React.FC = () => { const [formData, setFormData] = useState<LoginFormData>({ email: '', password: '', }); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); setError(null); const result = await authService.login(formData.email, formData.password); if (result.success) { window.location.href = '/dashboard'; } else { setError(result.error?.message || 'Erreur de connexion'); } setLoading(false); }; return ( <div className="login-container"> <form onSubmit={handleSubmit} className="login-form"> <h1>Connexion</h1> {error && <div className="error">{error}</div>} <div className="form-group"> <label htmlFor="email">Email</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} required placeholder="john@example.com" /> </div> <div className="form-group"> <label htmlFor="password">Mot de passe</label> <input type="password" id="password" name="password" value={formData.password} onChange={handleChange} required placeholder="••••••••" /> </div> <button type="submit" disabled={loading}> {loading ? 'Connexion...' : 'Se connecter'} </button> </form> </div> ); }; export default Login;
Composant Dashboard (avec utilisation de /me)
// components/Dashboard.tsx import React, { useEffect, useState } from 'react'; import { authService } from '../services/auth.service'; interface User { id: number; name: string; email: string; emailVerifiedAt: string | null; createdAt: string; updatedAt: string; } const Dashboard: React.FC = () => { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchUser = async () => { setLoading(true); const result = await authService.me(); if (result.success) { setUser(result.data); } else { setError(result.error?.message || 'Erreur de chargement'); } setLoading(false); }; fetchUser(); }, []); const handleLogout = async () => { await authService.logout(); window.location.href = '/login'; }; if (loading) { return <div>Chargement...</div>; } if (error || !user) { return <div>Erreur: {error || 'Utilisateur non trouvé'}</div>; } return ( <div className="dashboard"> <h1>Bienvenue {user.name} 👋</h1> <div className="user-info"> <p><strong>Email:</strong> {user.email}</p> <p><strong>Vérifié:</strong> {user.emailVerifiedAt ? '✅ Oui' : '❌ Non'}</p> <p><strong>Inscrit le:</strong> {new Date(user.createdAt).toLocaleDateString()}</p> </div> <button onClick={handleLogout}>Déconnexion</button> </div> ); }; export default Dashboard;