google2fa = new Google2FA(); } /** * Show 2FA setup page (generate QR code data). */ public function show() { $user = auth()->user(); // If not yet set up, generate a new secret if (!$user->two_factor_secret) { $secret = $this->google2fa->generateSecretKey(); $user->update(['two_factor_secret' => $secret]); } $secret = $user->two_factor_secret; $otpUrl = $this->google2fa->getQRCodeUrl( config('app.name'), $user->email, $secret ); // Generate QR code as SVG using BaconQrCode $renderer = new \BaconQrCode\Renderer\ImageRenderer( new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200), new \BaconQrCode\Renderer\Image\SvgImageBackEnd() ); $qrCode = (new \BaconQrCode\Writer($renderer))->writeString($otpUrl); $qrCodeBase64 = 'data:image/svg+xml;base64,' . base64_encode($qrCode); return Inertia::render('TwoFactor/Setup', [ 'enabled' => !is_null($user->two_factor_confirmed_at), 'qr_code' => $qrCodeBase64, 'secret' => $secret, 'recovery_codes' => $user->two_factor_recovery_codes ? json_decode($user->two_factor_recovery_codes, true) : [], ]); } /** * Confirm & enable 2FA. */ public function enable(Request $request) { $request->validate([ 'code' => 'required|string', ]); $user = auth()->user(); $secret = $user->two_factor_secret; $valid = $this->google2fa->verifyKey($secret, $request->code); if (!$valid) { return back()->withErrors(['code' => 'Invalid authentication code. Please try again.']); } // Generate recovery codes $recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10)); $user->update([ 'two_factor_confirmed_at' => now(), 'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()), 'email_2fa_enabled' => false, // Automatically disable Email 2FA ]); return back()->with('success', 'Two-Factor Authentication has been enabled successfully.'); } /** * Disable 2FA. */ public function disable(Request $request) { $request->validate([ 'password' => 'required|current_password', ]); auth()->user()->update([ 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'two_factor_confirmed_at' => null, ]); return back()->with('success', 'Two-Factor Authentication has been disabled.'); } public function toggleEmail(Request $request) { $request->validate([ 'password' => 'required|current_password', 'enabled' => 'required|boolean', ]); $user = auth()->user(); if ($request->enabled) { // Live-verify SMTP configuration by sending a test validation email try { \Illuminate\Support\Facades\Mail::to($user->email)->send( new \App\Mail\Send2FACode('123456') ); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("SMTP verification failed: " . $e->getMessage()); return back()->withErrors([ 'password' => 'Cannot enable Email 2FA: Your SMTP mail configuration is invalid or not working. We tried to send a validation email but failed. Error: ' . $e->getMessage() ]); } $user->update([ 'email_2fa_enabled' => true, 'two_factor_secret' => null, 'two_factor_recovery_codes' => null, 'two_factor_confirmed_at' => null, ]); } else { $user->update([ 'email_2fa_enabled' => false, ]); } $status = $request->enabled ? 'enabled' : 'disabled'; return back()->with('success', "Two-Factor Authentication via Email has been {$status} successfully."); } /** * Regenerate recovery codes. */ public function regenerateCodes() { $recoveryCodes = Collection::times(8, fn() => Str::random(10) . '-' . Str::random(10)); auth()->user()->update([ 'two_factor_recovery_codes' => json_encode($recoveryCodes->toArray()), ]); return back()->with('success', 'Recovery codes have been regenerated.'); } /** * Show the 2FA challenge screen (after login). */ public function challenge(Request $request) { if (!$request->session()->has('two_factor_user_id')) { return redirect()->route('login'); } $type = $request->session()->get('two_factor_type', 'totp'); return Inertia::render('TwoFactor/Challenge', [ 'type' => $type, ]); } /** * Verify the 2FA challenge code after login. */ public function verify(Request $request) { $request->validate([ 'code' => 'required|string', ]); $userId = $request->session()->get('two_factor_user_id'); $type = $request->session()->get('two_factor_type', 'totp'); if (!$userId) { return redirect()->route('login'); } $user = \App\Models\User::find($userId); if (!$user) { $request->session()->forget(['two_factor_user_id', 'two_factor_type']); return redirect()->route('login'); } if ($type === 'email') { if (!$user->email_2fa_enabled || !$user->email_2fa_code) { $request->session()->forget(['two_factor_user_id', 'two_factor_type']); return redirect()->route('login'); } if ($user->email_2fa_code !== $request->code || !$user->email_2fa_expires_at || $user->email_2fa_expires_at->isPast()) { return back()->withErrors(['code' => 'Invalid or expired authentication code. Please try again.']); } // Code is valid! Clear it $user->update([ 'email_2fa_code' => null, 'email_2fa_expires_at' => null, ]); } else { if (!$user->two_factor_secret) { $request->session()->forget(['two_factor_user_id', 'two_factor_type']); return redirect()->route('login'); } $code = $request->code; $valid = $this->google2fa->verifyKey($user->two_factor_secret, $code); if (!$valid) { $recoveryCodes = json_decode($user->two_factor_recovery_codes ?? '[]', true); if (in_array($code, $recoveryCodes)) { $remaining = array_filter($recoveryCodes, fn($c) => $c !== $code); $user->update(['two_factor_recovery_codes' => json_encode(array_values($remaining))]); $valid = true; } } if (!$valid) { return back()->withErrors(['code' => 'Invalid code. Please try again.']); } } $request->session()->forget(['two_factor_user_id', 'two_factor_type']); \Illuminate\Support\Facades\Auth::login($user); $request->session()->regenerate(); return redirect()->intended(route('dashboard')); } /** * Resend Email 2FA verification code. */ public function resendCode(Request $request) { if (!$request->session()->has('two_factor_user_id') || $request->session()->get('two_factor_type') !== 'email') { return redirect()->route('login'); } $userId = $request->session()->get('two_factor_user_id'); $user = \App\Models\User::find($userId); if (!$user || !$user->email_2fa_enabled) { return redirect()->route('login'); } $code = str_pad(mt_rand(100000, 999999), 6, '0', STR_PAD_LEFT); $user->update([ 'email_2fa_code' => $code, 'email_2fa_expires_at' => now()->addMinutes(10), ]); try { \Illuminate\Support\Facades\Mail::to($user->email)->send(new \App\Mail\Send2FACode($code)); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("Failed to resend 2FA Email Code: " . $e->getMessage()); return back()->withErrors(['code' => 'Failed to send email. Please check SMTP configuration or try again.']); } return back()->with('success', 'A new verification code has been sent to your email.'); } }