diff --git a/app/Http/Controllers/Admin/SettingController.php b/app/Http/Controllers/Admin/SettingController.php index 3e6c8bd..d0a7a60 100644 --- a/app/Http/Controllers/Admin/SettingController.php +++ b/app/Http/Controllers/Admin/SettingController.php @@ -5,10 +5,14 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Services\SiteSettingsService; use App\Services\UpdateService; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; use Illuminate\View\View; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; class SettingController extends Controller { @@ -61,6 +65,87 @@ class SettingController extends Controller return back()->with('success', 'Logo supprimé.'); } + // ── SMTP ────────────────────────────────────────────────────────────────── + + public function updateSmtp(Request $request): RedirectResponse + { + $data = $request->validate([ + 'smtp_host' => ['required', 'string', 'max:255'], + 'smtp_port' => ['required', 'integer', 'min:1', 'max:65535'], + 'smtp_encryption' => ['nullable', 'in:tls,ssl'], + 'smtp_username' => ['nullable', 'string', 'max:255'], + 'smtp_password' => ['nullable', 'string', 'max:255'], + 'smtp_from_address' => ['required', 'email', 'max:255'], + 'smtp_from_name' => ['required', 'string', 'max:255'], + ]); + + SiteSettingsService::set('smtp', [ + 'host' => $data['smtp_host'], + 'port' => (int) $data['smtp_port'], + 'encryption' => $data['smtp_encryption'] ?? null, + 'username' => $data['smtp_username'] ?? null, + 'password' => $data['smtp_password'] ?? null, + 'from_address' => $data['smtp_from_address'], + 'from_name' => $data['smtp_from_name'], + ]); + + return back()->with('success', 'Configuration SMTP enregistrée. Le 2FA par e-mail est maintenant actif.'); + } + + public function deleteSmtp(): RedirectResponse + { + SiteSettingsService::set('smtp', []); + + return back()->with('success', 'Configuration SMTP supprimée. Le 2FA est désactivé.'); + } + + public function testSmtp(Request $request): JsonResponse + { + $data = $request->validate([ + 'smtp_host' => ['required', 'string'], + 'smtp_port' => ['required', 'integer'], + 'smtp_encryption' => ['nullable', 'in:tls,ssl'], + 'smtp_username' => ['nullable', 'string'], + 'smtp_password' => ['nullable', 'string'], + 'smtp_from_address' => ['required', 'email'], + 'smtp_from_name' => ['required', 'string'], + ]); + + try { + $useSsl = ($data['smtp_encryption'] ?? '') === 'ssl'; + $transport = new EsmtpTransport($data['smtp_host'], (int) $data['smtp_port'], $useSsl); + + if (! empty($data['smtp_username'])) { + $transport->setUsername($data['smtp_username']); + $transport->setPassword($data['smtp_password'] ?? ''); + } + + $mailer = new \Symfony\Component\Mailer\Mailer($transport); + + $email = (new Email()) + ->from(new Address($data['smtp_from_address'], $data['smtp_from_name'])) + ->to(auth()->user()->email) + ->subject('Test SMTP — ' . config('app.name')) + ->text( + "Ce message confirme que votre configuration SMTP fonctionne correctement.\n\n" . + "Serveur : {$data['smtp_host']}:{$data['smtp_port']}\n" . + "Chiffrement : " . ($data['smtp_encryption'] ?: 'aucun') . "\n\n" . + "— " . config('app.name') + ); + + $mailer->send($email); + + return response()->json([ + 'ok' => true, + 'message' => 'E-mail de test envoyé à ' . auth()->user()->email . '. Vérifiez votre boîte de réception.', + ]); + } catch (\Throwable $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], 422); + } + } + + // ── Paramètres généraux ─────────────────────────────────────────────────── + public function updateSettings(Request $request): RedirectResponse { $data = $request->validate([ diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php index 613bcd9..f27ffa4 100644 --- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -4,42 +4,68 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Http\Requests\Auth\LoginRequest; +use App\Mail\TwoFactorPinMail; +use App\Services\SiteSettingsService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Illuminate\View\View; class AuthenticatedSessionController extends Controller { - /** - * Display the login view. - */ public function create(): View { return view('auth.login'); } - /** - * Handle an incoming authentication request. - */ public function store(LoginRequest $request): RedirectResponse { $request->authenticate(); + // Si SMTP configuré → 2FA par code PIN + if (SiteSettingsService::smtpConfigured()) { + $user = Auth::user(); + $intended = $request->session()->pull('url.intended', route('dashboard')); + + $pin = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); + + // Déconnecter et stocker les données 2FA dans la session + Auth::logout(); + $request->session()->regenerate(); + $request->session()->put([ + '2fa.user_id' => $user->id, + '2fa.pin_hash' => Hash::make($pin), + '2fa.expires_at' => now()->addMinutes(10)->timestamp, + '2fa.intended' => $intended, + ]); + + try { + Mail::to($user->email)->send(new TwoFactorPinMail($pin, $user->name)); + } catch (\Exception $e) { + // Si l'envoi échoue, on logue l'erreur et on laisse passer sans 2FA + Log::error('2FA: envoi du PIN impossible.', ['error' => $e->getMessage()]); + $request->session()->forget('2fa'); + Auth::login($user); + $request->session()->regenerate(); + return redirect($intended); + } + + return redirect()->route('2fa.challenge'); + } + $request->session()->regenerate(); return redirect()->intended(route('dashboard', absolute: false)); } - /** - * Destroy an authenticated session. - */ public function destroy(Request $request): RedirectResponse { Auth::guard('web')->logout(); $request->session()->invalidate(); - $request->session()->regenerateToken(); return redirect('/'); diff --git a/app/Http/Controllers/Auth/TwoFactorController.php b/app/Http/Controllers/Auth/TwoFactorController.php new file mode 100644 index 0000000..491130d --- /dev/null +++ b/app/Http/Controllers/Auth/TwoFactorController.php @@ -0,0 +1,103 @@ +session()->has('2fa.user_id')) { + return redirect()->route('login'); + } + + $user = User::find($request->session()->get('2fa.user_id')); + if (! $user) { + $request->session()->forget('2fa'); + return redirect()->route('login'); + } + + return view('auth.two-factor', [ + 'maskedEmail' => $this->maskEmail($user->email), + ]); + } + + public function verify(Request $request): RedirectResponse + { + $request->validate(['pin' => ['required', 'string', 'digits:6']]); + + $userId = $request->session()->get('2fa.user_id'); + $pinHash = $request->session()->get('2fa.pin_hash'); + $expiresAt = $request->session()->get('2fa.expires_at'); + + if (! $userId || ! $pinHash) { + return redirect()->route('login') + ->withErrors(['email' => 'Session expirée. Veuillez vous reconnecter.']); + } + + if (now()->timestamp > (int) $expiresAt) { + $request->session()->forget('2fa'); + return redirect()->route('login') + ->withErrors(['email' => 'Le code PIN a expiré. Veuillez vous reconnecter.']); + } + + if (! Hash::check($request->input('pin'), $pinHash)) { + return back()->withErrors(['pin' => 'Code incorrect. Vérifiez votre e-mail et réessayez.']); + } + + // PIN valide — authentifier l'utilisateur + $user = User::findOrFail($userId); + $intended = $request->session()->pull('2fa.intended', route('dashboard')); + + $request->session()->forget('2fa'); + Auth::login($user); + $request->session()->regenerate(); + + return redirect($intended); + } + + public function resend(Request $request): RedirectResponse + { + $userId = $request->session()->get('2fa.user_id'); + if (! $userId) { + return redirect()->route('login'); + } + + $user = User::find($userId); + if (! $user) { + return redirect()->route('login'); + } + + $pin = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); + + $request->session()->put([ + '2fa.pin_hash' => Hash::make($pin), + '2fa.expires_at' => now()->addMinutes(10)->timestamp, + ]); + + try { + Mail::to($user->email)->send(new TwoFactorPinMail($pin, $user->name)); + } catch (\Exception $e) { + return back()->withErrors(['pin' => "Impossible d'envoyer le code : " . $e->getMessage()]); + } + + return back()->with('resent', true); + } + + private function maskEmail(string $email): string + { + [$local, $domain] = explode('@', $email, 2); + $visible = min(2, strlen($local)); + $masked = substr($local, 0, $visible) . str_repeat('*', max(strlen($local) - $visible, 3)); + return $masked . '@' . $domain; + } +} diff --git a/app/Mail/TwoFactorPinMail.php b/app/Mail/TwoFactorPinMail.php new file mode 100644 index 0000000..3529e66 --- /dev/null +++ b/app/Mail/TwoFactorPinMail.php @@ -0,0 +1,32 @@ + SiteSettingsService::siteName()]); + + // Applique la configuration SMTP si elle est définie dans les paramètres + if (SiteSettingsService::smtpConfigured()) { + $smtp = SiteSettingsService::smtpConfig(); + config([ + 'mail.default' => 'smtp', + 'mail.mailers.smtp.host' => $smtp['host'], + 'mail.mailers.smtp.port' => (int) ($smtp['port'] ?? 587), + 'mail.mailers.smtp.encryption' => $smtp['encryption'] ?: null, + 'mail.mailers.smtp.username' => $smtp['username'] ?? null, + 'mail.mailers.smtp.password' => $smtp['password'] ?? null, + 'mail.from.address' => $smtp['from_address'] ?? null, + 'mail.from.name' => $smtp['from_name'] ?? SiteSettingsService::siteName(), + ]); + } } } diff --git a/app/Services/SiteSettingsService.php b/app/Services/SiteSettingsService.php index f52e68d..0b0e879 100644 --- a/app/Services/SiteSettingsService.php +++ b/app/Services/SiteSettingsService.php @@ -60,6 +60,19 @@ class SiteSettingsService } } + // ── SMTP ───────────────────────────────────────────────────────────────── + + public static function smtpConfig(): array + { + return self::get('smtp', []); + } + + public static function smtpConfigured(): bool + { + $smtp = self::smtpConfig(); + return ! empty($smtp['host']) && ! empty($smtp['port']); + } + // ── Titre du site ───────────────────────────────────────────────────────── public static function siteName(): string diff --git a/resources/views/admin/parametres/index.blade.php b/resources/views/admin/parametres/index.blade.php index 9125309..0c3b46e 100644 --- a/resources/views/admin/parametres/index.blade.php +++ b/resources/views/admin/parametres/index.blade.php @@ -48,6 +48,168 @@ + {{-- SMTP --}} + @php $smtp = \App\Services\SiteSettingsService::smtpConfig(); @endphp +
+ Quand configuré, la connexion nécessitera un code PIN envoyé par e-mail (2FA). +
+
+ Un code à 6 chiffres a été envoyé à
+ {{ $maskedEmail }}
+
Bonjour {{ $userName }},
+Voici votre code de connexion à usage unique :
+ +Ce code expire dans 10 minutes.
+Si vous n'avez pas essayé de vous connecter, ignorez cet e-mail et votre compte restera sécurisé.
+