From f781cd9bec9a98255f5ec9aa438e40bc3851dcc4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:46:53 +0000 Subject: [PATCH] Changes --- .../security/TotpEnrollmentWizard.tsx | 310 ++++++++++++++++++ src/components/security/TotpRemoveDialog.tsx | 104 ++++++ src/pages/user/SecurityPage.tsx | 55 +++- 3 files changed, 462 insertions(+), 7 deletions(-) create mode 100644 src/components/security/TotpEnrollmentWizard.tsx create mode 100644 src/components/security/TotpRemoveDialog.tsx diff --git a/src/components/security/TotpEnrollmentWizard.tsx b/src/components/security/TotpEnrollmentWizard.tsx new file mode 100644 index 0000000..5aafd75 --- /dev/null +++ b/src/components/security/TotpEnrollmentWizard.tsx @@ -0,0 +1,310 @@ +import { useState } from "react"; +import { Smartphone, Copy, CheckCircle, Loader2, AlertCircle, ShieldCheck } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useToast } from "@/hooks/use-toast"; + +type WizardStep = "setup" | "verify" | "backup-codes" | "success"; + +interface TotpEnrollmentWizardProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess: () => void; +} + +export function TotpEnrollmentWizard({ + open, + onOpenChange, + onSuccess, +}: TotpEnrollmentWizardProps) { + const [step, setStep] = useState("setup"); + const [isLoading, setIsLoading] = useState(false); + const [verificationCode, setVerificationCode] = useState(""); + const [verifyError, setVerifyError] = useState(null); + const [copiedSecret, setCopiedSecret] = useState(false); + const [copiedBackupCodes, setCopiedBackupCodes] = useState(false); + const { toast } = useToast(); + + // Mock data - will be replaced with actual API calls + const mockSecret = "JBSWY3DPEHPK3PXP"; + const mockQrCode = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=otpauth://totp/Gatehouse:user@example.com?secret=${mockSecret}&issuer=Gatehouse`; + const mockBackupCodes = [ + "ABCD-1234-EFGH", + "IJKL-5678-MNOP", + "QRST-9012-UVWX", + "YZAB-3456-CDEF", + "GHIJ-7890-KLMN", + "OPQR-1234-STUV", + "WXYZ-5678-ABCD", + "EFGH-9012-IJKL", + ]; + + const resetWizard = () => { + setStep("setup"); + setVerificationCode(""); + setVerifyError(null); + setCopiedSecret(false); + setCopiedBackupCodes(false); + setIsLoading(false); + }; + + const handleClose = (isOpen: boolean) => { + if (!isOpen) { + resetWizard(); + } + onOpenChange(isOpen); + }; + + const handleCopySecret = async () => { + try { + await navigator.clipboard.writeText(mockSecret); + setCopiedSecret(true); + setTimeout(() => setCopiedSecret(false), 2000); + } catch (err) { + console.error("Failed to copy secret:", err); + } + }; + + const handleCopyBackupCodes = async () => { + try { + await navigator.clipboard.writeText(mockBackupCodes.join("\n")); + setCopiedBackupCodes(true); + toast({ + title: "Backup codes copied", + description: "Store these codes in a safe place.", + }); + setTimeout(() => setCopiedBackupCodes(false), 2000); + } catch (err) { + console.error("Failed to copy backup codes:", err); + } + }; + + const handleVerify = async () => { + if (verificationCode.length !== 6) { + setVerifyError("Please enter a 6-digit code"); + return; + } + + setIsLoading(true); + setVerifyError(null); + + try { + // TODO: Call actual API to verify TOTP code + // await api.users.verifyTotpEnrollment(verificationCode); + + // Mock verification - accept "123456" for testing + await new Promise((resolve) => setTimeout(resolve, 1000)); + + if (verificationCode === "123456") { + setStep("backup-codes"); + } else { + setVerifyError("Invalid verification code. Please try again."); + } + } catch (err) { + console.error("TOTP verification failed:", err); + setVerifyError("Verification failed. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const handleComplete = () => { + onSuccess(); + handleClose(false); + toast({ + title: "Two-factor authentication enabled", + description: "Your account is now protected with TOTP.", + }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && step === "verify" && verificationCode.length === 6) { + handleVerify(); + } + }; + + return ( + + + + + + {step === "setup" && "Set up Authenticator App"} + {step === "verify" && "Verify Setup"} + {step === "backup-codes" && "Save Backup Codes"} + {step === "success" && "Setup Complete"} + + + {step === "setup" && "Scan the QR code with your authenticator app"} + {step === "verify" && "Enter the code from your authenticator app"} + {step === "backup-codes" && "Save these codes in a safe place"} + {step === "success" && "Two-factor authentication is now enabled"} + + + +
+ {step === "setup" && ( + <> +
+ TOTP QR Code +
+ +
+ +
+ + {mockSecret} + + +
+
+ +
+ + +
+ + )} + + {step === "verify" && ( + <> +
+ + { + const value = e.target.value.replace(/\D/g, ""); + setVerificationCode(value); + setVerifyError(null); + }} + onKeyDown={handleKeyDown} + disabled={isLoading} + className="text-center text-2xl tracking-widest font-mono" + autoFocus + /> + {verifyError && ( +
+ + {verifyError} +
+ )} +
+ +

+ Open your authenticator app and enter the 6-digit code shown for Gatehouse. +

+ +
+ + +
+ + )} + + {step === "backup-codes" && ( + <> +
+
+ +
+

Save these backup codes

+

+ You'll need these if you lose access to your authenticator app. + Each code can only be used once. +

+
+
+
+ +
+ {mockBackupCodes.map((code, index) => ( + + {code} + + ))} +
+ + + +
+ +
+ + )} +
+
+
+ ); +} diff --git a/src/components/security/TotpRemoveDialog.tsx b/src/components/security/TotpRemoveDialog.tsx new file mode 100644 index 0000000..7b70ea4 --- /dev/null +++ b/src/components/security/TotpRemoveDialog.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { AlertTriangle, Loader2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { useToast } from "@/hooks/use-toast"; + +interface TotpRemoveDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess: () => void; + isRequired?: boolean; +} + +export function TotpRemoveDialog({ + open, + onOpenChange, + onSuccess, + isRequired = false, +}: TotpRemoveDialogProps) { + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const handleRemove = async () => { + setIsLoading(true); + + try { + // TODO: Call actual API to remove TOTP + // await api.users.removeTotpEnrollment(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + toast({ + title: "Two-factor authentication disabled", + description: "TOTP has been removed from your account.", + }); + + onSuccess(); + onOpenChange(false); + } catch (err) { + console.error("Failed to remove TOTP:", err); + toast({ + title: "Failed to remove TOTP", + description: "An error occurred. Please try again.", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + + + + Remove Two-Factor Authentication? + + +

+ This will disable TOTP-based two-factor authentication for your account. + Your backup codes will also be invalidated. +

+ + {isRequired && ( +
+ Warning: Your organization requires two-factor authentication. + You may lose access to certain features if you disable it. +
+ )} + +

+ Are you sure you want to continue? +

+
+
+ +
+ + +
+
+
+ ); +} diff --git a/src/pages/user/SecurityPage.tsx b/src/pages/user/SecurityPage.tsx index d742956..0bbe670 100644 --- a/src/pages/user/SecurityPage.tsx +++ b/src/pages/user/SecurityPage.tsx @@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { AddPasskeyWizard } from "@/components/security/AddPasskeyWizard"; +import { TotpEnrollmentWizard } from "@/components/security/TotpEnrollmentWizard"; +import { TotpRemoveDialog } from "@/components/security/TotpRemoveDialog"; import { PasswordStrengthMeter, isPasswordValid } from "@/components/auth/PasswordStrengthMeter"; import { api, ApiError } from "@/lib/api"; import { useToast } from "@/hooks/use-toast"; @@ -13,6 +15,8 @@ import { useToast } from "@/hooks/use-toast"; export default function SecurityPage() { const [showPasswordForm, setShowPasswordForm] = useState(false); const [showAddPasskey, setShowAddPasskey] = useState(false); + const [showTotpEnrollment, setShowTotpEnrollment] = useState(false); + const [showTotpRemove, setShowTotpRemove] = useState(false); // Password form state const [currentPassword, setCurrentPassword] = useState(""); @@ -21,12 +25,14 @@ export default function SecurityPage() { const [isChangingPassword, setIsChangingPassword] = useState(false); const [passwordError, setPasswordError] = useState(null); + // TOTP state + const [totpEnabled, setTotpEnabled] = useState(false); + const { toast } = useToast(); // Mock security data const security = { passwordLastChanged: "3 months ago", - totpEnabled: true, passkeysCount: 2, passkeys: [ { id: "1", name: "MacBook Pro Touch ID", lastUsed: "Today" }, @@ -228,21 +234,37 @@ export default function SecurityPage() { Use an authenticator app for two-factor authentication - {security.totpEnabled ? ( + {totpEnabled ? ( Enabled ) : ( - + )} - {security.totpEnabled && ( + {totpEnabled && ( - +
+ + +
)} @@ -303,6 +325,25 @@ export default function SecurityPage() { setShowAddPasskey(false); }} /> + + { + setTotpEnabled(true); + setShowTotpEnrollment(false); + }} + /> + + { + setTotpEnabled(false); + setShowTotpRemove(false); + }} + isRequired={security.policyRequirements.totpRequired} + /> ); }