style consistency changes to add security key form

This commit is contained in:
miloschwartz 2025-07-14 15:43:33 -07:00
parent 0a97d91aed
commit 3505342a8d
No known key found for this signature in database
4 changed files with 645 additions and 329 deletions

View file

@ -1143,8 +1143,7 @@
"securityKeyNameRequired": "Name is required",
"securityKeyRemove": "Remove",
"securityKeyLastUsed": "Last used: {date}",
"securityKeyNameLabel": "Name",
"securityKeyNamePlaceholder": "Enter a name for this security key",
"securityKeyNameLabel": "Security Key Name",
"securityKeyRegisterSuccess": "Security key registered successfully",
"securityKeyRegisterError": "Failed to register security key",
"securityKeyRemoveSuccess": "Security key removed successfully",
@ -1152,7 +1151,7 @@
"securityKeyLoadError": "Failed to load security keys",
"securityKeyLogin": "Sign in with security key",
"securityKeyAuthError": "Failed to authenticate with security key",
"securityKeyRecommendation": "Tip: Register a backup security key on another device to ensure you always have access to your account.",
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
"registering": "Registering...",
"securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.",
"securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.",
@ -1163,5 +1162,16 @@
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
"twoFactor": "Two-Factor Authentication",
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
"continueToApplication": "Continue to Application"
"continueToApplication": "Continue to Application",
"securityKeyAdd": "Add Security Key",
"securityKeyRegisterTitle": "Register New Security Key",
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
"securityKeyTwoFactorRequired": "Two-Factor Authentication Required",
"securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key",
"securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key",
"securityKeyTwoFactorCode": "Two-Factor Code",
"securityKeyRemoveTitle": "Remove Security Key",
"securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"",
"securityKeyNoKeysRegistered": "No security keys registered",
"securityKeyNoKeysDescription": "Add a security key to enhance your account security"
}

2
package-lock.json generated
View file

@ -33,9 +33,9 @@
"@radix-ui/react-toast": "1.2.14",
"@react-email/components": "0.3.1",
"@react-email/render": "^1.1.2",
"@react-email/tailwind": "1.2.1",
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^9.0.3",
"@react-email/tailwind": "1.2.1",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0",

View file

@ -107,7 +107,8 @@ async function clearChallenge(sessionId: string) {
export const registerSecurityKeyBody = z.object({
name: z.string().min(1),
password: z.string().min(1)
password: z.string().min(1),
code: z.string().optional()
}).strict();
export const verifyRegistrationBody = z.object({
@ -143,7 +144,7 @@ export async function startRegistration(
);
}
const { name, password } = parsedBody.data;
const { name, password, code } = parsedBody.data;
const user = req.user as User;
// Only allow internal users to use security keys
@ -163,6 +164,39 @@ export async function startRegistration(
return next(unauthorized());
}
// If user has 2FA enabled, require and verify the code
if (user.twoFactorEnabled) {
if (!code) {
return response<{ codeRequested: boolean }>(res, {
data: { codeRequested: true },
success: true,
error: false,
message: "Two-factor authentication required",
status: HttpCode.ACCEPTED
});
}
const validOTP = await verifyTotpCode(
code,
user.twoFactorSecret!,
user.userId
);
if (!validOTP) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Two-factor code incorrect. Email: ${user.email}. IP: ${req.ip}.`
);
}
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"The two-factor code you entered is incorrect"
)
);
}
}
// Get existing security keys for user
const existingSecurityKeys = await db
.select()

View file

@ -15,23 +15,25 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@app/components/ui/dialog";
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle
} from "@app/components/Credenza";
import { startRegistration } from "@simplewebauthn/browser";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Card, CardContent } from "@app/components/ui/card";
import { Badge } from "@app/components/ui/badge";
import { Loader2, KeyRound, Trash2, Plus, Shield } from "lucide-react";
import { Loader2, KeyRound, Trash2, Plus, Shield, Info } from "lucide-react";
import { cn } from "@app/lib/cn";
type SecurityKeyFormProps = {
@ -53,6 +55,7 @@ type DeleteSecurityKeyData = {
type RegisterFormValues = {
name: string;
password: string;
code?: string;
};
type DeleteFormValues = {
@ -70,30 +73,47 @@ type FieldProps = {
};
};
export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps) {
export default function SecurityKeyForm({
open,
setOpen
}: SecurityKeyFormProps) {
const t = useTranslations();
const { env } = useEnvContext();
const api = createApiClient({ env });
const [securityKeys, setSecurityKeys] = useState<SecurityKey[]>([]);
const [isRegistering, setIsRegistering] = useState(false);
const [showRegisterDialog, setShowRegisterDialog] = useState(false);
const [selectedSecurityKey, setSelectedSecurityKey] = useState<DeleteSecurityKeyData | null>(null);
const [show2FADialog, setShow2FADialog] = useState(false);
const [dialogState, setDialogState] = useState<
"list" | "register" | "register2fa" | "delete" | "delete2fa"
>("list");
const [selectedSecurityKey, setSelectedSecurityKey] =
useState<DeleteSecurityKeyData | null>(null);
const [deleteInProgress, setDeleteInProgress] = useState(false);
const [pendingDeleteCredentialId, setPendingDeleteCredentialId] = useState<string | null>(null);
const [pendingDeletePassword, setPendingDeletePassword] = useState<string | null>(null);
const [pendingDeleteCredentialId, setPendingDeleteCredentialId] = useState<
string | null
>(null);
const [pendingDeletePassword, setPendingDeletePassword] = useState<
string | null
>(null);
const [pendingRegisterData, setPendingRegisterData] = useState<{
name: string;
password: string;
} | null>(null);
const [register2FAForm, setRegister2FAForm] = useState<{ code: string }>({
code: ""
});
useEffect(() => {
loadSecurityKeys();
}, []);
const registerSchema = z.object({
name: z.string().min(1, { message: t('securityKeyNameRequired') }),
password: z.string().min(1, { message: t('passwordRequired') }),
name: z.string().min(1, { message: t("securityKeyNameRequired") }),
password: z.string().min(1, { message: t("passwordRequired") }),
code: z.string().optional()
});
const deleteSchema = z.object({
password: z.string().min(1, { message: t('passwordRequired') }),
password: z.string().min(1, { message: t("passwordRequired") }),
code: z.string().optional()
});
@ -102,7 +122,8 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
defaultValues: {
name: "",
password: "",
},
code: ""
}
});
const deleteForm = useForm<DeleteFormValues>({
@ -110,7 +131,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
defaultValues: {
password: "",
code: ""
},
}
});
const loadSecurityKeys = async () => {
@ -120,7 +141,7 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
} catch (error) {
toast({
variant: "destructive",
description: formatAxiosError(error, t('securityKeyLoadError')),
description: formatAxiosError(error, t("securityKeyLoadError"))
});
}
};
@ -131,26 +152,32 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
if (!window.PublicKeyCredential) {
toast({
variant: "destructive",
description: t('securityKeyBrowserNotSupported', {
defaultValue: "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari."
description: t("securityKeyBrowserNotSupported", {
defaultValue:
"Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari."
})
});
return;
}
setIsRegistering(true);
const startRes = await api.post("/auth/security-key/register/start", {
const startRes = await api.post(
"/auth/security-key/register/start",
{
name: values.name,
password: values.password,
});
code: values.code
}
);
if (startRes.status === 202) {
toast({
variant: "destructive",
description: t('twoFactorRequired', {
defaultValue: "Two-factor authentication is required to register a security key."
})
// If 2FA is required
if (startRes.status === 202 && startRes.data.data?.codeRequested) {
setPendingRegisterData({
name: values.name,
password: values.password
});
setDialogState("register2fa");
setIsRegistering(false);
return;
}
@ -160,59 +187,66 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
const credential = await startRegistration(options);
await api.post("/auth/security-key/register/verify", {
credential,
credential
});
toast({
description: t('securityKeyRegisterSuccess', {
description: t("securityKeyRegisterSuccess", {
defaultValue: "Security key registered successfully"
})
});
registerForm.reset();
setShowRegisterDialog(false);
setDialogState("list");
await loadSecurityKeys();
} catch (error: any) {
if (error.name === 'NotAllowedError') {
if (error.message.includes('denied permission')) {
if (error.name === "NotAllowedError") {
if (error.message.includes("denied permission")) {
toast({
variant: "destructive",
description: t('securityKeyPermissionDenied', {
defaultValue: "Please allow access to your security key to continue registration."
description: t("securityKeyPermissionDenied", {
defaultValue:
"Please allow access to your security key to continue registration."
})
});
} else {
toast({
variant: "destructive",
description: t('securityKeyRemovedTooQuickly', {
defaultValue: "Please keep your security key connected until the registration process completes."
description: t("securityKeyRemovedTooQuickly", {
defaultValue:
"Please keep your security key connected until the registration process completes."
})
});
}
} else if (error.name === 'NotSupportedError') {
} else if (error.name === "NotSupportedError") {
toast({
variant: "destructive",
description: t('securityKeyNotSupported', {
defaultValue: "Your security key may not be compatible. Please try a different security key."
description: t("securityKeyNotSupported", {
defaultValue:
"Your security key may not be compatible. Please try a different security key."
})
});
} else {
toast({
variant: "destructive",
description: t('securityKeyUnknownError', {
defaultValue: "There was a problem registering your security key. Please try again."
description: t("securityKeyUnknownError", {
defaultValue:
"There was a problem registering your security key. Please try again."
})
});
}
throw error; // Re-throw to be caught by outer catch
}
} catch (error) {
console.error('Security key registration error:', error);
console.error("Security key registration error:", error);
toast({
variant: "destructive",
description: formatAxiosError(error, t('securityKeyRegisterError', {
description: formatAxiosError(
error,
t("securityKeyRegisterError", {
defaultValue: "Failed to register security key"
}))
})
)
});
} finally {
setIsRegistering(false);
@ -224,33 +258,42 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
try {
setDeleteInProgress(true);
const encodedCredentialId = encodeURIComponent(selectedSecurityKey.credentialId);
const response = await api.delete(`/auth/security-key/${encodedCredentialId}`, {
const encodedCredentialId = encodeURIComponent(
selectedSecurityKey.credentialId
);
const response = await api.delete(
`/auth/security-key/${encodedCredentialId}`,
{
data: {
password: values.password,
code: values.code
}
});
}
);
// If 2FA is required
if (response.status === 202 && response.data.data.codeRequested) {
setPendingDeleteCredentialId(encodedCredentialId);
setPendingDeletePassword(values.password);
setShow2FADialog(true);
setDialogState("delete2fa");
return;
}
toast({
description: t('securityKeyRemoveSuccess')
description: t("securityKeyRemoveSuccess")
});
deleteForm.reset();
setSelectedSecurityKey(null);
setDialogState("list");
await loadSecurityKeys();
} catch (error) {
toast({
variant: "destructive",
description: formatAxiosError(error, t('securityKeyRemoveError')),
description: formatAxiosError(
error,
t("securityKeyRemoveError")
)
});
} finally {
setDeleteInProgress(false);
@ -262,33 +305,128 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
try {
setDeleteInProgress(true);
await api.delete(`/auth/security-key/${pendingDeleteCredentialId}`, {
await api.delete(
`/auth/security-key/${pendingDeleteCredentialId}`,
{
data: {
password: pendingDeletePassword,
code: values.code
}
});
}
);
toast({
description: t('securityKeyRemoveSuccess')
description: t("securityKeyRemoveSuccess")
});
deleteForm.reset();
setSelectedSecurityKey(null);
setShow2FADialog(false);
setDialogState("list");
setPendingDeleteCredentialId(null);
setPendingDeletePassword(null);
await loadSecurityKeys();
} catch (error) {
toast({
variant: "destructive",
description: formatAxiosError(error, t('securityKeyRemoveError')),
description: formatAxiosError(
error,
t("securityKeyRemoveError")
)
});
} finally {
setDeleteInProgress(false);
}
};
const handleRegister2FASubmit = async (values: { code: string }) => {
if (!pendingRegisterData) return;
try {
setIsRegistering(true);
const startRes = await api.post(
"/auth/security-key/register/start",
{
name: pendingRegisterData.name,
password: pendingRegisterData.password,
code: values.code
}
);
const options = startRes.data.data;
try {
const credential = await startRegistration(options);
await api.post("/auth/security-key/register/verify", {
credential
});
toast({
description: t("securityKeyRegisterSuccess", {
defaultValue: "Security key registered successfully"
})
});
registerForm.reset();
setDialogState("list");
setPendingRegisterData(null);
setRegister2FAForm({ code: "" });
await loadSecurityKeys();
} catch (error: any) {
if (error.name === "NotAllowedError") {
if (error.message.includes("denied permission")) {
toast({
variant: "destructive",
description: t("securityKeyPermissionDenied", {
defaultValue:
"Please allow access to your security key to continue registration."
})
});
} else {
toast({
variant: "destructive",
description: t("securityKeyRemovedTooQuickly", {
defaultValue:
"Please keep your security key connected until the registration process completes."
})
});
}
} else if (error.name === "NotSupportedError") {
toast({
variant: "destructive",
description: t("securityKeyNotSupported", {
defaultValue:
"Your security key may not be compatible. Please try a different security key."
})
});
} else {
toast({
variant: "destructive",
description: t("securityKeyUnknownError", {
defaultValue:
"There was a problem registering your security key. Please try again."
})
});
}
throw error; // Re-throw to be caught by outer catch
}
} catch (error) {
console.error("Security key registration error:", error);
toast({
variant: "destructive",
description: formatAxiosError(
error,
t("securityKeyRegisterError", {
defaultValue: "Failed to register security key"
})
)
});
setRegister2FAForm({ code: "" });
} finally {
setIsRegistering(false);
}
};
const onOpenChange = (open: boolean) => {
if (open) {
loadSecurityKeys();
@ -296,60 +434,89 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
registerForm.reset();
deleteForm.reset();
setSelectedSecurityKey(null);
setShowRegisterDialog(false);
setDialogState("list");
setPendingRegisterData(null);
setRegister2FAForm({ code: "" });
}
setOpen(open);
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
{t('securityKeyManage')}
</DialogTitle>
<DialogDescription>
{t('securityKeyDescription')}
</DialogDescription>
</DialogHeader>
<Credenza open={open} onOpenChange={onOpenChange}>
<CredenzaContent>
{dialogState === "list" && (
<>
<CredenzaHeader>
<CredenzaTitle className="flex items-center gap-2">
{t("securityKeyManage")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-muted-foreground">{t('securityKeyList')}</h3>
<h3 className="text-sm font-medium text-muted-foreground">
{t("securityKeyList")}
</h3>
<Button
className="h-8 w-8 p-0"
onClick={() => setShowRegisterDialog(true)}
onClick={() =>
setDialogState("register")
}
className="gap-2"
>
<Plus className="h-4 w-4" />
{t("securityKeyAdd")}
</Button>
</div>
{securityKeys.length > 0 ? (
<div className="space-y-2">
{securityKeys.map((securityKey) => (
<Card key={securityKey.credentialId}>
<Card
key={
securityKey.credentialId
}
>
<CardContent className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-secondary">
<KeyRound className="h-4 w-4 text-secondary-foreground" />
</div>
<div>
<p className="font-medium">{securityKey.name}</p>
<p className="font-medium">
{
securityKey.name
}
</p>
<p className="text-xs text-muted-foreground">
{t('securityKeyLastUsed', {
date: new Date(securityKey.lastUsed).toLocaleDateString()
})}
{t(
"securityKeyLastUsed",
{
date: new Date(
securityKey.lastUsed
).toLocaleDateString()
}
)}
</p>
</div>
</div>
<Button
className="h-8 w-8 p-0 text-white hover:text-white/80"
onClick={() => setSelectedSecurityKey({
credentialId: securityKey.credentialId,
onClick={() => {
setSelectedSecurityKey(
{
credentialId:
securityKey.credentialId,
name: securityKey.name
})}
}
);
setDialogState(
"delete"
);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
@ -360,42 +527,66 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Shield className="mb-2 h-12 w-12 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No security keys registered</p>
<p className="text-xs text-muted-foreground">Add a security key to enhance your account security</p>
<p className="text-sm text-muted-foreground">
{t("securityKeyNoKeysRegistered")}
</p>
<p className="text-xs text-muted-foreground">
{t("securityKeyNoKeysDescription")}
</p>
</div>
)}
{securityKeys.length === 1 && (
<Alert variant="default">
<AlertDescription>{t('securityKeyRecommendation')}</AlertDescription>
<Info className="h-4 w-4" />
<AlertDescription>
{t("securityKeyRecommendation")}
</AlertDescription>
</Alert>
)}
</div>
</DialogContent>
</Dialog>
<Dialog open={showRegisterDialog} onOpenChange={setShowRegisterDialog}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Register New Security Key</DialogTitle>
<DialogDescription>
Connect your security key and enter a name to identify it
</DialogDescription>
</DialogHeader>
</CredenzaBody>
</>
)}
{dialogState === "register" && (
<>
<CredenzaHeader>
<CredenzaTitle>
{t("securityKeyRegisterTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyRegisterDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...registerForm}>
<form onSubmit={registerForm.handleSubmit(handleRegisterSecurityKey)} className="space-y-4">
<form
onSubmit={registerForm.handleSubmit(
handleRegisterSecurityKey
)}
className="space-y-4"
id="form"
>
<FormField
control={registerForm.control}
name="name"
render={({ field }: FieldProps) => (
<FormItem>
<FormLabel>{t('securityKeyNameLabel')}</FormLabel>
<FormLabel>
{t(
"securityKeyNameLabel"
)}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t('securityKeyNamePlaceholder')}
disabled={isRegistering}
placeholder={t(
"securityKeyNamePlaceholder"
)}
disabled={
isRegistering
}
/>
</FormControl>
<FormMessage />
@ -407,181 +598,262 @@ export default function SecurityKeyForm({ open, setOpen }: SecurityKeyFormProps)
name="password"
render={({ field }: FieldProps) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>
{t("password")}
</FormLabel>
<FormControl>
<Input
{...field}
type="password"
disabled={isRegistering}
disabled={
isRegistering
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button
type="button"
className="border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground"
variant="outline"
onClick={() => {
registerForm.reset();
setShowRegisterDialog(false);
setDialogState("list");
}}
disabled={isRegistering}
>
{t('cancel')}
{t("cancel")}
</Button>
</CredenzaClose>
<Button
type="submit"
form="form"
disabled={isRegistering}
className={cn(
"min-w-[100px]",
isRegistering && "cursor-not-allowed opacity-50"
isRegistering &&
"cursor-not-allowed opacity-50"
)}
loading={isRegistering}
>
{isRegistering ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('registering')}
</>
) : (
t('securityKeyRegister')
)}
{t("securityKeyRegister")}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</CredenzaFooter>
</>
)}
<Dialog open={!!selectedSecurityKey} onOpenChange={(open) => !open && setSelectedSecurityKey(null)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<Trash2 className="h-4 w-4" />
Remove Security Key
</DialogTitle>
<DialogDescription>
Enter your password to remove the security key "{selectedSecurityKey?.name}"
</DialogDescription>
</DialogHeader>
{dialogState === "register2fa" && (
<>
<CredenzaHeader>
<CredenzaTitle>
{t("securityKeyTwoFactorRequired")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyTwoFactorDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">
{t("securityKeyTwoFactorCode")}
</label>
<Input
type="text"
value={register2FAForm.code}
onChange={(e) =>
setRegister2FAForm({
code: e.target.value
})
}
maxLength={6}
disabled={isRegistering}
/>
</div>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button
variant="outline"
onClick={() => {
setRegister2FAForm({ code: "" });
setDialogState("list");
setPendingRegisterData(null);
}}
disabled={isRegistering}
>
{t("cancel")}
</Button>
</CredenzaClose>
<Button
type="button"
className="min-w-[100px]"
disabled={
isRegistering ||
register2FAForm.code.length !== 6
}
loading={isRegistering}
onClick={() =>
handleRegister2FASubmit({
code: register2FAForm.code
})
}
>
{t("securityKeyRegister")}
</Button>
</CredenzaFooter>
</>
)}
{dialogState === "delete" && (
<>
<CredenzaHeader>
<CredenzaTitle className="flex items-center gap-2">
{t("securityKeyRemoveTitle")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyRemoveDescription", { name: selectedSecurityKey!.name! })}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...deleteForm}>
<form onSubmit={deleteForm.handleSubmit(handleDeleteSecurityKey)} className="space-y-4">
<form
onSubmit={deleteForm.handleSubmit(
handleDeleteSecurityKey
)}
className="space-y-4"
id="delete-form"
>
<FormField
control={deleteForm.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormLabel>
{t("password")}
</FormLabel>
<FormControl>
<Input
{...field}
type="password"
disabled={deleteInProgress}
disabled={
deleteInProgress
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button
type="button"
className="border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground"
variant="outline"
onClick={() => {
deleteForm.reset();
setSelectedSecurityKey(null);
setDialogState("list");
}}
disabled={deleteInProgress}
>
{t('cancel')}
{t("cancel")}
</Button>
</CredenzaClose>
<Button
type="submit"
form="delete-form"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={deleteInProgress}
loading={deleteInProgress}
>
{deleteInProgress ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('securityKeyRemoving')}
</>
) : (
t('securityKeyRemove')
)}
{t("securityKeyRemove")}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
<Dialog open={show2FADialog} onOpenChange={(open) => !open && setShow2FADialog(false)}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Two-Factor Authentication Required</DialogTitle>
<DialogDescription>
Please enter your two-factor authentication code to remove the security key
</DialogDescription>
</DialogHeader>
</CredenzaFooter>
</>
)}
{dialogState === "delete2fa" && (
<>
<CredenzaHeader>
<CredenzaTitle>
{t("securityKeyTwoFactorRequired")}
</CredenzaTitle>
<CredenzaDescription>
{t("securityKeyTwoFactorRemoveDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...deleteForm}>
<form onSubmit={deleteForm.handleSubmit(handle2FASubmit)} className="space-y-4">
<form
onSubmit={deleteForm.handleSubmit(
handle2FASubmit
)}
className="space-y-4"
id="delete2fa-form"
>
<FormField
control={deleteForm.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel>Two-Factor Code</FormLabel>
<FormLabel>
{t("securityKeyTwoFactorCode")}
</FormLabel>
<FormControl>
<Input
{...field}
type="text"
placeholder="Enter your 6-digit code"
disabled={deleteInProgress}
maxLength={6}
disabled={
deleteInProgress
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button
type="button"
className="border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground"
variant="outline"
onClick={() => {
deleteForm.reset();
setShow2FADialog(false);
setDialogState("list");
setPendingDeleteCredentialId(null);
setPendingDeletePassword(null);
}}
disabled={deleteInProgress}
>
{t('cancel')}
{t("cancel")}
</Button>
</CredenzaClose>
<Button
type="submit"
form="delete2fa-form"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
disabled={deleteInProgress}
loading={deleteInProgress}
>
{deleteInProgress ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('securityKeyRemoving')}
</>
) : (
t('securityKeyRemove')
)}
{t("securityKeyRemove")}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</CredenzaFooter>
</>
)}
</CredenzaContent>
</Credenza>
</>
);
}