middleware('auth')->except(['showChallenge', 'verifyChallenge']); } // POST /2fa/setup — generate secret + return QR SVG public function setup() { $user = Auth::user(); $google2fa = app('pragmarx.google2fa'); $secret = $google2fa->generateSecretKey(); session(['2fa_setup_secret' => $secret]); $qrUrl = $google2fa->getQRCodeUrl( config('app.name'), $user->email, $secret ); $renderer = new \BaconQrCode\Renderer\ImageRenderer( new \BaconQrCode\Renderer\RendererStyle\RendererStyle(200), new \BaconQrCode\Renderer\Image\SvgImageBackEnd() ); $writer = new \BaconQrCode\Writer($renderer); $qrSvg = base64_encode($writer->writeString($qrUrl)); return response()->json([ 'secret' => $secret, 'qr' => $qrSvg, ]); } // POST /2fa/enable — confirm OTP and save secret public function enable(Request $request) { $request->validate(['code' => 'required|digits:6']); $user = Auth::user(); $google2fa = app('pragmarx.google2fa'); $secret = session('2fa_setup_secret'); if (! $secret || ! $google2fa->verifyKey($secret, $request->code)) { return back()->with('toast_error', 'Invalid code — please try again.'); } $user->update([ 'two_factor_secret' => encrypt($secret), 'two_factor_enabled' => true, ]); session()->forget('2fa_setup_secret'); AuditLog::record('user.2fa.enabled'); return redirect()->route('channel')->with('toast_success', 'Two-factor authentication enabled!')->with('_open_tab', 'settings'); } // POST /2fa/disable — verifies password then sends a confirmation email public function disable(Request $request) { $request->validate(['password' => 'required']); $user = Auth::user(); if (! \Hash::check($request->password, $user->password)) { return back()->with('toast_error', 'Incorrect password.')->with('_open_tab', 'settings'); } $confirmUrl = URL::temporarySignedRoute( '2fa.disable.confirm', now()->addMinutes(15), ['user' => $user->id] ); Mail::to($user->email)->send(new TwoFactorDisableConfirmation($user, $confirmUrl)); AuditLog::record('user.2fa.disable_requested'); return redirect()->route('channel') ->with('toast_success', 'Check your email — a confirmation link has been sent to ' . $user->email . '.') ->with('_open_tab', 'settings'); } // GET /2fa/disable/confirm?signature=... public function confirmDisable(Request $request) { if (! $request->hasValidSignature()) { abort(403, 'This confirmation link is invalid or has expired.'); } $user = \App\Models\User::findOrFail($request->query('user')); // Ensure the signed URL belongs to the currently authenticated user (or log them in if // they arrive via email while not logged in on this device) if (Auth::check() && Auth::id() !== $user->id) { abort(403, 'This confirmation link belongs to a different account.'); } if (! $user->two_factor_enabled) { return redirect()->route('channel') ->with('toast_success', 'Two-factor authentication is already disabled.') ->with('_open_tab', 'settings'); } $user->update([ 'two_factor_secret' => null, 'two_factor_enabled' => false, ]); AuditLog::record('user.2fa.disabled', ['user_id' => $user->id, 'user_name' => $user->name]); if (! Auth::check()) { return redirect()->route('login') ->with('toast_success', 'Two-factor authentication has been disabled. Please log in.'); } return redirect()->route('channel') ->with('toast_success', 'Two-factor authentication has been disabled.') ->with('_open_tab', 'settings'); } // GET /2fa/challenge public function showChallenge() { if (! session('2fa_user_id')) { return redirect()->route('login'); } return view('auth.2fa-challenge'); } // POST /2fa/challenge public function verifyChallenge(Request $request) { $userId = session('2fa_user_id'); if (! $userId) { return redirect()->route('login'); } $request->validate(['code' => 'required|digits:6']); $user = \App\Models\User::findOrFail($userId); $google2fa = app('pragmarx.google2fa'); $secret = decrypt($user->two_factor_secret); if (! $google2fa->verifyKey($secret, $request->code)) { return back()->withErrors(['code' => 'Invalid code — please try again.']); } session()->forget('2fa_user_id'); Auth::login($user, session()->pull('2fa_remember', false)); return redirect()->intended('/videos'); } }