Enhance totp UI flow
- Add TOTP enrollment UI flow to SecurityPage via TotpEnrollmentWizard - Integrate removal dialog TotpRemoveDialog with confirmation - Update SecurityPage to reference new TOTP components and reflect enrollment state - Implement wizard steps: setup, verify, backup-codes, and completion - Show enabling status and removal option with confirmation when enrolled X-Lovable-Edit-ID: edt-8f92b58a-f7e2-4820-9941-aeb31a19c58f
This commit is contained in:
@@ -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<WizardStep>("setup");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [verificationCode, setVerificationCode] = useState("");
|
||||||
|
const [verifyError, setVerifyError] = useState<string | null>(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 (
|
||||||
|
<Dialog open={open} onOpenChange={handleClose}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Smartphone className="w-5 h-5" />
|
||||||
|
{step === "setup" && "Set up Authenticator App"}
|
||||||
|
{step === "verify" && "Verify Setup"}
|
||||||
|
{step === "backup-codes" && "Save Backup Codes"}
|
||||||
|
{step === "success" && "Setup Complete"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{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"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{step === "setup" && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center p-4 bg-white rounded-lg">
|
||||||
|
<img
|
||||||
|
src={mockQrCode}
|
||||||
|
alt="TOTP QR Code"
|
||||||
|
className="w-48 h-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs text-muted-foreground">
|
||||||
|
Can't scan? Enter this code manually:
|
||||||
|
</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<code className="flex-1 px-3 py-2 text-sm font-mono bg-muted rounded-md break-all">
|
||||||
|
{mockSecret}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCopySecret}
|
||||||
|
>
|
||||||
|
{copiedSecret ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-success" />
|
||||||
|
) : (
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button variant="outline" onClick={() => handleClose(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setStep("verify")}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "verify" && (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="totp-code">Verification code</Label>
|
||||||
|
<Input
|
||||||
|
id="totp-code"
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
maxLength={6}
|
||||||
|
placeholder="000000"
|
||||||
|
value={verificationCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
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 && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-destructive">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
{verifyError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Open your authenticator app and enter the 6-digit code shown for Gatehouse.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setStep("setup");
|
||||||
|
setVerificationCode("");
|
||||||
|
setVerifyError(null);
|
||||||
|
}}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleVerify}
|
||||||
|
disabled={isLoading || verificationCode.length !== 6}
|
||||||
|
>
|
||||||
|
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "backup-codes" && (
|
||||||
|
<>
|
||||||
|
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
||||||
|
<div className="flex gap-2 text-warning">
|
||||||
|
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium">Save these backup codes</p>
|
||||||
|
<p className="text-warning/80">
|
||||||
|
You'll need these if you lose access to your authenticator app.
|
||||||
|
Each code can only be used once.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 p-4 bg-muted rounded-lg">
|
||||||
|
{mockBackupCodes.map((code, index) => (
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
className="px-2 py-1 text-sm font-mono text-center bg-background rounded"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleCopyBackupCodes}
|
||||||
|
>
|
||||||
|
{copiedBackupCodes ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2 text-success" />
|
||||||
|
Copied!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Copy all codes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button onClick={handleComplete}>
|
||||||
|
<ShieldCheck className="w-4 h-4 mr-2" />
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-destructive" />
|
||||||
|
Remove Two-Factor Authentication?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-3">
|
||||||
|
<p>
|
||||||
|
This will disable TOTP-based two-factor authentication for your account.
|
||||||
|
Your backup codes will also be invalidated.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{isRequired && (
|
||||||
|
<div className="p-3 bg-destructive/10 border border-destructive/30 rounded-lg text-destructive text-sm">
|
||||||
|
<strong>Warning:</strong> Your organization requires two-factor authentication.
|
||||||
|
You may lose access to certain features if you disable it.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-sm">
|
||||||
|
Are you sure you want to continue?
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleRemove}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
||||||
|
Remove TOTP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { AddPasskeyWizard } from "@/components/security/AddPasskeyWizard";
|
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 { PasswordStrengthMeter, isPasswordValid } from "@/components/auth/PasswordStrengthMeter";
|
||||||
import { api, ApiError } from "@/lib/api";
|
import { api, ApiError } from "@/lib/api";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -13,6 +15,8 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
export default function SecurityPage() {
|
export default function SecurityPage() {
|
||||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||||
const [showAddPasskey, setShowAddPasskey] = useState(false);
|
const [showAddPasskey, setShowAddPasskey] = useState(false);
|
||||||
|
const [showTotpEnrollment, setShowTotpEnrollment] = useState(false);
|
||||||
|
const [showTotpRemove, setShowTotpRemove] = useState(false);
|
||||||
|
|
||||||
// Password form state
|
// Password form state
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
@@ -21,12 +25,14 @@ export default function SecurityPage() {
|
|||||||
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
||||||
const [passwordError, setPasswordError] = useState<string | null>(null);
|
const [passwordError, setPasswordError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// TOTP state
|
||||||
|
const [totpEnabled, setTotpEnabled] = useState(false);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
// Mock security data
|
// Mock security data
|
||||||
const security = {
|
const security = {
|
||||||
passwordLastChanged: "3 months ago",
|
passwordLastChanged: "3 months ago",
|
||||||
totpEnabled: true,
|
|
||||||
passkeysCount: 2,
|
passkeysCount: 2,
|
||||||
passkeys: [
|
passkeys: [
|
||||||
{ id: "1", name: "MacBook Pro Touch ID", lastUsed: "Today" },
|
{ 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
|
Use an authenticator app for two-factor authentication
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{security.totpEnabled ? (
|
{totpEnabled ? (
|
||||||
<Badge className="bg-success/10 text-success border-0">
|
<Badge className="bg-success/10 text-success border-0">
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
Enabled
|
Enabled
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Button size="sm">Set up</Button>
|
<Button size="sm" onClick={() => setShowTotpEnrollment(true)}>
|
||||||
|
Set up
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{security.totpEnabled && (
|
{totpEnabled && (
|
||||||
<CardContent className="border-t pt-4">
|
<CardContent className="border-t pt-4">
|
||||||
<Button variant="outline" size="sm">
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowTotpEnrollment(true)}
|
||||||
|
>
|
||||||
Reconfigure
|
Reconfigure
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
onClick={() => setShowTotpRemove(true)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -303,6 +325,25 @@ export default function SecurityPage() {
|
|||||||
setShowAddPasskey(false);
|
setShowAddPasskey(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TotpEnrollmentWizard
|
||||||
|
open={showTotpEnrollment}
|
||||||
|
onOpenChange={setShowTotpEnrollment}
|
||||||
|
onSuccess={() => {
|
||||||
|
setTotpEnabled(true);
|
||||||
|
setShowTotpEnrollment(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TotpRemoveDialog
|
||||||
|
open={showTotpRemove}
|
||||||
|
onOpenChange={setShowTotpRemove}
|
||||||
|
onSuccess={() => {
|
||||||
|
setTotpEnabled(false);
|
||||||
|
setShowTotpRemove(false);
|
||||||
|
}}
|
||||||
|
isRequired={security.policyRequirements.totpRequired}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user