@hed-hog/core 0.0.185 → 0.0.190

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
  2. package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
  3. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
  4. package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
  5. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
  6. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
  7. package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
  8. package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
  9. package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
  10. package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
  11. package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
  12. package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
  13. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
  14. package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
  15. package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
  16. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
  17. package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
  18. package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
  19. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
  20. package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
  21. package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
  22. package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
  23. package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
  24. package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
  25. package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
  26. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
  27. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
  28. package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
  29. package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
  30. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
  31. package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
  34. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
  35. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
  36. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
  37. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
  38. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
  39. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
  40. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
  41. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
  42. package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
  43. package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
  44. package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
  45. package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
  46. package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
  47. package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
  48. package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
  49. package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
  50. package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
  51. package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
  52. package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
  53. package/hedhog/frontend/messages/en.json +1080 -0
  54. package/hedhog/frontend/messages/pt.json +1135 -0
  55. package/package.json +4 -4
@@ -0,0 +1,192 @@
1
+ import { Badge } from '@/components/ui/badge';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Checkbox } from '@/components/ui/checkbox';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from '@/components/ui/dialog';
12
+ import { Label } from '@/components/ui/label';
13
+ import { useApp } from '@hed-hog/next-app-provider';
14
+ import { CheckedState } from '@radix-ui/react-checkbox';
15
+ import { Copy, Download, Shield } from 'lucide-react';
16
+ import { useTranslations } from 'next-intl';
17
+ import { useCallback, useState } from 'react';
18
+ import { copyToClipboard } from '../lib/mfa-utils';
19
+
20
+ interface RecoveryCodesDialogProps {
21
+ open: boolean;
22
+ codes: string[];
23
+ onOpenChange: (open: boolean) => void;
24
+ onConfirm: () => void;
25
+ }
26
+
27
+ export function RecoveryCodesDialog({
28
+ open,
29
+ codes,
30
+ onOpenChange,
31
+ onConfirm,
32
+ }: RecoveryCodesDialogProps) {
33
+ const t = useTranslations('core.RecoveryCodes');
34
+ const [checkSavedCodes, setCheckSavedCodes] = useState<CheckedState>(false);
35
+ const [showError, setShowError] = useState(false);
36
+ const { showToastHandler } = useApp();
37
+
38
+ const handleCopyCode = async (code: string) => {
39
+ const success = await copyToClipboard(code);
40
+ if (success) {
41
+ showToastHandler('success', t('codeCopied'));
42
+ } else {
43
+ showToastHandler('error', t('codeCopyFailed'));
44
+ }
45
+ };
46
+
47
+ const handleCopyAllCodes = async () => {
48
+ const codesText = codes.join('\n');
49
+ const success = await copyToClipboard(codesText);
50
+ if (success) {
51
+ showToastHandler('success', t('allCodesCopied'));
52
+ } else {
53
+ showToastHandler('error', t('allCodesCopyFailed'));
54
+ }
55
+ };
56
+
57
+ const handleDownloadCodes = useCallback(() => {
58
+ if (!codes || codes.length === 0) {
59
+ showToastHandler('error', t('noCodesAvailable'));
60
+ return;
61
+ }
62
+ try {
63
+ const content = codes.join('\n');
64
+ const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
65
+ const url = URL.createObjectURL(blob);
66
+ const a = document.createElement('a');
67
+ a.href = url;
68
+ a.download = 'recovery-codes.txt';
69
+ document.body.appendChild(a);
70
+ a.click();
71
+ a.remove();
72
+ URL.revokeObjectURL(url);
73
+ showToastHandler('success', t('codesDownloaded'));
74
+ } catch (e) {
75
+ showToastHandler('error', t('downloadFailed'));
76
+ }
77
+ }, [codes, showToastHandler, t]);
78
+
79
+ const handleConfirm = () => {
80
+ if (!checkSavedCodes) {
81
+ setShowError(true);
82
+ return;
83
+ }
84
+
85
+ try {
86
+ onConfirm();
87
+ } finally {
88
+ setShowError(false);
89
+ onOpenChange(false);
90
+ }
91
+ };
92
+
93
+ const handleOpenChange = (open: boolean) => {
94
+ if (!open && !checkSavedCodes) {
95
+ setShowError(true);
96
+ return;
97
+ }
98
+ setShowError(false);
99
+ onOpenChange(open);
100
+ };
101
+
102
+ return (
103
+ <Dialog open={open} onOpenChange={handleOpenChange}>
104
+ <DialogContent className="max-w-md">
105
+ <DialogHeader>
106
+ <DialogTitle className="flex items-center gap-2">
107
+ <Shield className="size-5 text-primary" />
108
+ {t('title')}
109
+ </DialogTitle>
110
+ <DialogDescription>{t('description')}</DialogDescription>
111
+ </DialogHeader>
112
+ <div className="space-y-4 py-4">
113
+ <div className="rounded-lg border bg-muted/50 p-4">
114
+ <div className="grid grid-cols-2 gap-3">
115
+ {codes.map((code, index) => (
116
+ <div
117
+ key={index}
118
+ className="font-mono text-sm bg-background rounded pl-3 py-2 text-center font-semibold tracking-wider border flex items-center justify-between"
119
+ >
120
+ <span className="break-all">{code}</span>
121
+ <Button
122
+ variant="ghost"
123
+ size="icon"
124
+ onClick={() => handleCopyCode(code)}
125
+ >
126
+ <Copy className="w-4 h-4" />
127
+ </Button>
128
+ </div>
129
+ ))}
130
+ </div>
131
+ </div>
132
+ <div className="bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg p-3">
133
+ <p className="text-xs text-amber-800 dark:text-amber-200 font-medium">
134
+ {t('warningMessage')}
135
+ </p>
136
+ </div>
137
+ <div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-900 rounded-lg p-3">
138
+ <p className="text-xs text-blue-800 dark:text-blue-200 font-medium">
139
+ {t('infoMessage')}
140
+ </p>
141
+ </div>
142
+ </div>
143
+ <DialogFooter className="flex-col sm:flex-col gap-2">
144
+ <Button
145
+ variant="outline"
146
+ className="w-full"
147
+ onClick={handleCopyAllCodes}
148
+ disabled={!codes.length}
149
+ >
150
+ {t('copyAllCodes')}
151
+ </Button>
152
+ <Button
153
+ variant="outline"
154
+ className="w-full"
155
+ onClick={handleDownloadCodes}
156
+ disabled={!codes.length}
157
+ >
158
+ <Download className="w-4 h-4 mr-2" />
159
+ {t('downloadCodes')}
160
+ </Button>
161
+ <div className="w-full mt-4">
162
+ <Label
163
+ htmlFor="saved-codes-checkbox"
164
+ className="flex items-center gap-2 cursor-pointer select-none"
165
+ >
166
+ <Checkbox
167
+ id="saved-codes-checkbox"
168
+ checked={checkSavedCodes}
169
+ onCheckedChange={(checked) => {
170
+ setCheckSavedCodes(checked);
171
+ if (checked) setShowError(false);
172
+ }}
173
+ className="h-4 w-4 rounded border-muted bg-background"
174
+ />
175
+ <span className="text-sm font-medium">{t('confirmSaved')}</span>
176
+ </Label>
177
+
178
+ {showError && (
179
+ <Badge className="w-full text-sm my-4 bg-red-400 text-white font-medium">
180
+ {t('errorNotConfirmed')}
181
+ </Badge>
182
+ )}
183
+
184
+ <Button className="w-full mt-2" onClick={handleConfirm}>
185
+ {t('confirmAndClose')}
186
+ </Button>
187
+ </div>
188
+ </DialogFooter>
189
+ </DialogContent>
190
+ </Dialog>
191
+ );
192
+ }
@@ -0,0 +1,372 @@
1
+ import { Button } from '@/components/ui/button';
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from '@/components/ui/dialog';
10
+ import { Input } from '@/components/ui/input';
11
+ import {
12
+ InputOTP,
13
+ InputOTPGroup,
14
+ InputOTPSeparator,
15
+ InputOTPSlot,
16
+ } from '@/components/ui/input-otp';
17
+ import { Label } from '@/components/ui/label';
18
+ import { useApp } from '@hed-hog/next-app-provider';
19
+ import {
20
+ AlertTriangle,
21
+ Fingerprint,
22
+ Key,
23
+ Mail,
24
+ RefreshCw,
25
+ Shield,
26
+ } from 'lucide-react';
27
+ import { useTranslations } from 'next-intl';
28
+ import { useEffect, useState } from 'react';
29
+
30
+ interface RegenerateCodesDialogProps {
31
+ open: boolean;
32
+ onOpenChange: (open: boolean) => void;
33
+ onConfirm: (
34
+ verificationCode: string,
35
+ hash?: string,
36
+ useTotp?: boolean,
37
+ verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn',
38
+ assertionResponse?: any
39
+ ) => void;
40
+ requiresMfa: boolean;
41
+ verificationType?: 'totp' | 'email';
42
+ availableMethods?: ('totp' | 'email')[];
43
+ codeHash?: string;
44
+ hasWebAuthn?: boolean;
45
+ hasRecoveryCodes?: boolean;
46
+ }
47
+
48
+ export function RegenerateCodesDialog({
49
+ open,
50
+ onOpenChange,
51
+ onConfirm,
52
+ requiresMfa,
53
+ verificationType,
54
+ availableMethods,
55
+ codeHash,
56
+ hasWebAuthn,
57
+ hasRecoveryCodes,
58
+ }: RegenerateCodesDialogProps) {
59
+ const t = useTranslations('core.RegenerateRecoveryCodes');
60
+ const [verificationCode, setVerificationCode] = useState('');
61
+ const [loading, setLoading] = useState(false);
62
+ const [resendLoading, setResendLoading] = useState(false);
63
+ const [resendCooldown, setResendCooldown] = useState(0);
64
+
65
+ const getInitialMethod = (): 'totp' | 'email' | 'recovery' | 'webauthn' => {
66
+ if (verificationType)
67
+ return verificationType as 'totp' | 'email' | 'recovery' | 'webauthn';
68
+ if (availableMethods && availableMethods.length > 0)
69
+ return availableMethods[0] as 'totp' | 'email' | 'recovery' | 'webauthn';
70
+ if (hasWebAuthn) return 'recovery';
71
+ return 'totp';
72
+ };
73
+
74
+ const [selectedMethod, setSelectedMethod] = useState<
75
+ 'totp' | 'email' | 'recovery' | 'webauthn'
76
+ >(getInitialMethod());
77
+ const { getSettingValue, request, showToastHandler } = useApp();
78
+ const pinCodeLength = getSettingValue('mfa-email-code-length') || 6;
79
+
80
+ useEffect(() => {
81
+ if (resendCooldown > 0) {
82
+ const timer = setTimeout(
83
+ () => setResendCooldown(resendCooldown - 1),
84
+ 1000
85
+ );
86
+ return () => clearTimeout(timer);
87
+ }
88
+ }, [resendCooldown]);
89
+
90
+ useEffect(() => {
91
+ if (open) {
92
+ setSelectedMethod(getInitialMethod());
93
+ }
94
+ }, [open, verificationType, availableMethods, hasWebAuthn]);
95
+
96
+ const hasBothMethods = availableMethods && availableMethods.length > 0;
97
+ const showMethodToggle = hasBothMethods || hasWebAuthn || hasRecoveryCodes;
98
+ const currentMethod = selectedMethod;
99
+
100
+ const handleResendCode = async () => {
101
+ setResendLoading(true);
102
+ try {
103
+ await request({
104
+ url: '/profile/recovery-codes/send-verification',
105
+ method: 'POST',
106
+ });
107
+ setResendCooldown(30);
108
+ showToastHandler?.('success', t('codeResent'));
109
+ } catch (error) {
110
+ showToastHandler?.('error', t('resendFailed'));
111
+ } finally {
112
+ setResendLoading(false);
113
+ }
114
+ };
115
+
116
+ const handleConfirm = async () => {
117
+ if (requiresMfa && !verificationCode) {
118
+ return;
119
+ }
120
+
121
+ setLoading(true);
122
+ try {
123
+ let useTotp = false;
124
+ let hashToUse = undefined;
125
+
126
+ if (currentMethod === 'totp') {
127
+ useTotp = true;
128
+ } else if (currentMethod === 'email') {
129
+ hashToUse = codeHash;
130
+ }
131
+
132
+ onConfirm(verificationCode, hashToUse, useTotp, currentMethod);
133
+ setVerificationCode('');
134
+ } catch (error) {
135
+ console.error('Verification failed:', error);
136
+ } finally {
137
+ setLoading(false);
138
+ }
139
+ };
140
+
141
+ const handleOpenChange = (open: boolean) => {
142
+ if (!open) {
143
+ setVerificationCode('');
144
+ setSelectedMethod(getInitialMethod());
145
+ }
146
+ onOpenChange(open);
147
+ };
148
+
149
+ return (
150
+ <Dialog open={open} onOpenChange={handleOpenChange}>
151
+ <DialogContent className="sm:max-w-md">
152
+ <DialogHeader>
153
+ <DialogTitle className="flex items-center gap-2">
154
+ <AlertTriangle className="size-5 text-amber-500" />
155
+ {t('title')}
156
+ </DialogTitle>
157
+ <DialogDescription>{t('description')}</DialogDescription>
158
+ </DialogHeader>
159
+ <div className="space-y-4 py-4">
160
+ <div className="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-900 dark:bg-amber-950/20">
161
+ <p className="text-sm font-medium text-amber-800 dark:text-amber-200">
162
+ {t('warningMessage')}
163
+ </p>
164
+ </div>
165
+
166
+ {requiresMfa && (
167
+ <div className="space-y-2">
168
+ {showMethodToggle && (
169
+ <div className="flex justify-center gap-2 mb-4 flex-wrap">
170
+ {(availableMethods?.includes('totp') ||
171
+ verificationType === 'totp') && (
172
+ <Button
173
+ type="button"
174
+ variant={
175
+ selectedMethod === 'totp' ? 'default' : 'outline'
176
+ }
177
+ size="sm"
178
+ onClick={() => {
179
+ setSelectedMethod('totp');
180
+ setVerificationCode('');
181
+ }}
182
+ >
183
+ <Key className="mr-2 size-4" />
184
+ {t('buttonApp')}
185
+ </Button>
186
+ )}
187
+ {(availableMethods?.includes('email') ||
188
+ verificationType === 'email') && (
189
+ <Button
190
+ type="button"
191
+ variant={
192
+ selectedMethod === 'email' ? 'default' : 'outline'
193
+ }
194
+ size="sm"
195
+ onClick={() => {
196
+ setSelectedMethod('email');
197
+ setVerificationCode('');
198
+ }}
199
+ >
200
+ <Mail className="mr-2 size-4" />
201
+ {t('buttonEmail')}
202
+ </Button>
203
+ )}
204
+ {hasWebAuthn && (
205
+ <Button
206
+ type="button"
207
+ variant={
208
+ selectedMethod === 'webauthn' ? 'default' : 'outline'
209
+ }
210
+ size="sm"
211
+ onClick={async () => {
212
+ setLoading(true);
213
+ try {
214
+ const { startAuthentication } =
215
+ await import('@simplewebauthn/browser');
216
+
217
+ const { data: optionsData } = await request({
218
+ url: '/profile/webauthn/authenticate/generate',
219
+ method: 'POST',
220
+ });
221
+
222
+ const assertion = await startAuthentication(
223
+ optionsData as any
224
+ );
225
+ await onConfirm(
226
+ '',
227
+ undefined,
228
+ false,
229
+ 'webauthn',
230
+ assertion
231
+ );
232
+ setVerificationCode('');
233
+ setLoading(false);
234
+ handleOpenChange(false);
235
+ } catch (error: any) {
236
+ console.error('Verification failed:', error);
237
+ setLoading(false);
238
+ }
239
+ }}
240
+ disabled={loading}
241
+ >
242
+ <Fingerprint className="mr-2 size-4" />
243
+ {loading
244
+ ? t('buttonAuthenticating')
245
+ : t('buttonSecurityKey')}
246
+ </Button>
247
+ )}
248
+ {hasRecoveryCodes && (
249
+ <Button
250
+ type="button"
251
+ variant={
252
+ selectedMethod === 'recovery' ? 'default' : 'outline'
253
+ }
254
+ size="sm"
255
+ onClick={() => {
256
+ setSelectedMethod('recovery');
257
+ setVerificationCode('');
258
+ }}
259
+ >
260
+ <Shield className="mr-2 size-4" />
261
+ {t('buttonRecoveryCode')}
262
+ </Button>
263
+ )}
264
+ </div>
265
+ )}
266
+ <Label htmlFor="verification-code" className="text-center block">
267
+ {currentMethod === 'email'
268
+ ? t('labelEmail')
269
+ : currentMethod === 'recovery'
270
+ ? t('labelRecovery')
271
+ : currentMethod === 'webauthn'
272
+ ? t('labelWebAuthn')
273
+ : t('labelTotp')}
274
+ </Label>
275
+ {currentMethod === 'webauthn' ? (
276
+ <div className="text-center text-sm text-muted-foreground">
277
+ {t('webAuthnInstruction')}
278
+ </div>
279
+ ) : currentMethod === 'recovery' ? (
280
+ <div className="flex justify-center">
281
+ <Input
282
+ id="verification-code"
283
+ value={verificationCode}
284
+ onChange={(e) => setVerificationCode(e.target.value)}
285
+ className="text-center text-lg font-mono uppercase max-w-xs"
286
+ autoComplete="off"
287
+ placeholder={t('placeholderRecovery')}
288
+ />
289
+ </div>
290
+ ) : (
291
+ <div className="flex justify-center">
292
+ <InputOTP
293
+ maxLength={currentMethod === 'totp' ? 6 : pinCodeLength}
294
+ value={verificationCode}
295
+ onChange={setVerificationCode}
296
+ >
297
+ {(() => {
298
+ const length =
299
+ currentMethod === 'totp' ? 6 : pinCodeLength;
300
+ const groupSize = Math.ceil(length / 2);
301
+ const groups = [
302
+ Array.from({ length: groupSize }, (_, i) => (
303
+ <InputOTPSlot key={i} index={i} />
304
+ )),
305
+ Array.from({ length: length - groupSize }, (_, i) => (
306
+ <InputOTPSlot
307
+ key={i + groupSize}
308
+ index={i + groupSize}
309
+ />
310
+ )),
311
+ ];
312
+ return (
313
+ <>
314
+ <InputOTPGroup>{groups[0]}</InputOTPGroup>
315
+ <InputOTPSeparator />
316
+ <InputOTPGroup>{groups[1]}</InputOTPGroup>
317
+ </>
318
+ );
319
+ })()}
320
+ </InputOTP>
321
+ </div>
322
+ )}
323
+ {currentMethod === 'email' && (
324
+ <div className="flex justify-center">
325
+ <Button
326
+ type="button"
327
+ variant="outline"
328
+ size="sm"
329
+ onClick={handleResendCode}
330
+ disabled={resendLoading || resendCooldown > 0}
331
+ className="mt-2"
332
+ >
333
+ {resendLoading ? (
334
+ <>
335
+ <RefreshCw className="w-4 h-4 mr-2 animate-spin" />
336
+ {t('resending')}
337
+ </>
338
+ ) : resendCooldown > 0 ? (
339
+ <>
340
+ <RefreshCw className="w-4 h-4 mr-2" />
341
+ {t('resendIn', { seconds: resendCooldown })}
342
+ </>
343
+ ) : (
344
+ <>
345
+ <RefreshCw className="w-4 h-4 mr-2" />
346
+ {t('resendCode')}
347
+ </>
348
+ )}
349
+ </Button>
350
+ </div>
351
+ )}
352
+ </div>
353
+ )}
354
+ </div>
355
+ <DialogFooter>
356
+ <Button variant="outline" onClick={() => handleOpenChange(false)}>
357
+ {t('buttonCancel')}
358
+ </Button>
359
+ {currentMethod !== 'webauthn' && (
360
+ <Button
361
+ onClick={handleConfirm}
362
+ variant="default"
363
+ disabled={(requiresMfa && !verificationCode) || loading}
364
+ >
365
+ {loading ? t('buttonGenerating') : t('buttonGenerate')}
366
+ </Button>
367
+ )}
368
+ </DialogFooter>
369
+ </DialogContent>
370
+ </Dialog>
371
+ );
372
+ }