From 01c5dd7e14db961cf66c4c4997a0fbcaad100ea4 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sun, 27 Apr 2025 00:28:25 -0400 Subject: [PATCH 1/2] 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']) From ba416e1c74e339d69cefaaad2aae7c9ff8c34799 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Tue, 6 May 2025 01:45:49 -0400 Subject: [PATCH 2/2] update usages for consistency --- ...irms-password.tsx => confirm-password.tsx} | 30 ++++++++----------- resources/js/pages/settings/two-factor.tsx | 6 ++-- 2 files changed, 16 insertions(+), 20 deletions(-) rename resources/js/components/{confirms-password.tsx => confirm-password.tsx} (82%) diff --git a/resources/js/components/confirms-password.tsx b/resources/js/components/confirm-password.tsx similarity index 82% rename from resources/js/components/confirms-password.tsx rename to resources/js/components/confirm-password.tsx index aa7a0605..3a79baaa 100644 --- a/resources/js/components/confirms-password.tsx +++ b/resources/js/components/confirm-password.tsx @@ -3,9 +3,11 @@ 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'; + +import InputError from '@/components/input-error'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { useForm } from '@inertiajs/react'; interface Props { title?: string; @@ -14,7 +16,7 @@ interface Props { onConfirm(): void; } -export function ConfirmsPassword({ +export function ConfirmPassword({ title = 'Confirm Password', content = 'For your security, please confirm your password to continue.', button = 'Confirm', @@ -22,7 +24,7 @@ export function ConfirmsPassword({ children, }: PropsWithChildren) { const [confirmingPassword, setConfirmingPassword] = useState(false); - const [form, setForm] = useState({ + const form = useForm({ password: '', error: '', processing: false, @@ -42,29 +44,23 @@ export function ConfirmsPassword({ } function confirmPassword() { - setForm({ ...form, processing: true }); - axios .post(route('password.confirm'), { - password: form.password, + password: form.data.password, }) .then(() => { closeModal(); setTimeout(() => onConfirm(), 250); }) .catch((error) => { - setForm({ - ...form, - processing: false, - error: error.response.data.errors.password[0], - }); + form.setError('password', error.response.data.errors.password[0]); passwordRef.current?.focus(); }); } function closeModal() { setConfirmingPassword(false); - setForm({ processing: false, password: '', error: '' }); + form.reset(); } return ( @@ -91,10 +87,10 @@ export function ConfirmsPassword({ type="password" className="mt-1 block w-full" placeholder="Password" - value={form.password} - onChange={(e) => setForm({ ...form, password: e.currentTarget.value })} + value={form.data.password} + onChange={(e) => form.setData('password', e.currentTarget.value)} /> - + diff --git a/resources/js/pages/settings/two-factor.tsx b/resources/js/pages/settings/two-factor.tsx index b61e6d70..5b89f043 100644 --- a/resources/js/pages/settings/two-factor.tsx +++ b/resources/js/pages/settings/two-factor.tsx @@ -16,7 +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'; +import { ConfirmPassword } from '@/components/confirm-password'; interface TwoFactorProps { confirmed: boolean; @@ -295,13 +295,13 @@ export default function TwoFactor({ confirmed: initialConfirmed, recoveryCodes }
- - +
)}