From 01c5dd7e14db961cf66c4c4997a0fbcaad100ea4 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sun, 27 Apr 2025 00:28:25 -0400 Subject: [PATCH] require password when disabling 2fa --- .../Auth/ConfirmablePasswordController.php | 20 ++++ resources/js/components/confirms-password.tsx | 113 ++++++++++++++++++ resources/js/pages/settings/two-factor.tsx | 11 +- routes/auth.php | 3 + 4 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 resources/js/components/confirms-password.tsx diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php index c729706d..2abce9b7 100644 --- a/app/Http/Controllers/Auth/ConfirmablePasswordController.php +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -9,6 +9,7 @@ use Illuminate\Validation\ValidationException; use Inertia\Inertia; use Inertia\Response; +use Illuminate\Support\Facades\Date; class ConfirmablePasswordController extends Controller { @@ -38,4 +39,23 @@ public function store(Request $request): RedirectResponse return redirect()->intended(route('dashboard', absolute: false)); } + + public function status(Request $request) + { + $lastConfirmation = $request->session()->get( + 'auth.password_confirmed_at', 0 + ); + + $lastConfirmed = (Date::now()->unix() - $lastConfirmation); + + $confirmed = $lastConfirmed < $request->input( + 'seconds', config('auth.password_timeout', 900) + ); + + return response()->json([ + 'confirmed' => $confirmed, + ], headers: array_filter([ + 'X-Retry-After' => $confirmed ? $lastConfirmed : null, + ])); + } } diff --git a/resources/js/components/confirms-password.tsx b/resources/js/components/confirms-password.tsx new file mode 100644 index 00000000..aa7a0605 --- /dev/null +++ b/resources/js/components/confirms-password.tsx @@ -0,0 +1,113 @@ +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; +import axios from 'axios'; +import { Lock } from 'lucide-react'; +import { PropsWithChildren, useRef, useState } from 'react'; +import InputError from './input-error'; +import { Button } from './ui/button'; +import { Input } from './ui/input'; + +interface Props { + title?: string; + content?: string; + button?: string; + onConfirm(): void; +} + +export function ConfirmsPassword({ + title = 'Confirm Password', + content = 'For your security, please confirm your password to continue.', + button = 'Confirm', + onConfirm, + children, +}: PropsWithChildren) { + const [confirmingPassword, setConfirmingPassword] = useState(false); + const [form, setForm] = useState({ + password: '', + error: '', + processing: false, + }); + const passwordRef = useRef(null); + + function startConfirmingPassword() { + axios.get(route('password.confirmation')).then((response) => { + if (response.data.confirmed) { + onConfirm(); + } else { + setConfirmingPassword(true); + + setTimeout(() => passwordRef.current?.focus(), 250); + } + }); + } + + function confirmPassword() { + setForm({ ...form, processing: true }); + + axios + .post(route('password.confirm'), { + password: form.password, + }) + .then(() => { + closeModal(); + setTimeout(() => onConfirm(), 250); + }) + .catch((error) => { + setForm({ + ...form, + processing: false, + error: error.response.data.errors.password[0], + }); + passwordRef.current?.focus(); + }); + } + + function closeModal() { + setConfirmingPassword(false); + setForm({ processing: false, password: '', error: '' }); + } + + return ( + <> + + + {children} + + + +
+
+ +
+
+ {title} + {content} +
+ +
+
+ setForm({ ...form, password: e.currentTarget.value })} + /> + +
+
+ + + + + +
+
+ + ); +} diff --git a/resources/js/pages/settings/two-factor.tsx b/resources/js/pages/settings/two-factor.tsx index d2ebcdf8..b61e6d70 100644 --- a/resources/js/pages/settings/two-factor.tsx +++ b/resources/js/pages/settings/two-factor.tsx @@ -16,6 +16,7 @@ import { import { Check, Copy, Eye, EyeOff, Loader, ScanLine, LockKeyhole } from 'lucide-react'; import { useTwoFactorAuth } from '@/hooks/use-two-factor-auth'; import type { BreadcrumbItem } from '@/types'; +import { ConfirmsPassword } from '@/components/confirms-password'; interface TwoFactorProps { confirmed: boolean; @@ -294,9 +295,13 @@ export default function TwoFactor({ confirmed: initialConfirmed, recoveryCodes }
- + + +
)} diff --git a/routes/auth.php b/routes/auth.php index 8c22c675..852582d8 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -50,6 +50,9 @@ Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) ->name('password.confirm'); + Route::get('confirm-password/status', [ConfirmablePasswordController::class, 'status']) + ->name('password.confirmation'); + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])