@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,337 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from '@/components/ui/dialog';
12
+ import { Input } from '@/components/ui/input';
13
+ import {
14
+ InputOTP,
15
+ InputOTPGroup,
16
+ InputOTPSlot,
17
+ } from '@/components/ui/input-otp';
18
+ import { Label } from '@/components/ui/label';
19
+ import { useApp } from '@hed-hog/next-app-provider';
20
+ import { AlertTriangle, Fingerprint, Key, Mail, Shield } from 'lucide-react';
21
+ import { useTranslations } from 'next-intl';
22
+ import { useEffect, useState } from 'react';
23
+
24
+ interface RemoveMfaDialogProps {
25
+ open: boolean;
26
+ onOpenChange: (open: boolean) => void;
27
+ onConfirm: (
28
+ code: string,
29
+ hash?: string,
30
+ verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn',
31
+ assertionResponse?: any
32
+ ) => Promise<void>;
33
+ availableMethods?: ('totp' | 'email')[];
34
+ verificationType?: 'totp' | 'email';
35
+ codeHash?: string;
36
+ hasWebAuthn?: boolean;
37
+ methodName?: string;
38
+ }
39
+
40
+ export function RemoveMfaDialog({
41
+ open,
42
+ onOpenChange,
43
+ onConfirm,
44
+ availableMethods,
45
+ verificationType: initialVerificationType,
46
+ codeHash,
47
+ hasWebAuthn,
48
+ methodName,
49
+ }: RemoveMfaDialogProps) {
50
+ const [code, setCode] = useState('');
51
+ const [loading, setLoading] = useState(false);
52
+ const [resendLoading, setResendLoading] = useState(false);
53
+ const [resendCooldown, setResendCooldown] = useState(0);
54
+ const [selectedMethod, setSelectedMethod] = useState<
55
+ 'totp' | 'email' | 'recovery' | 'webauthn'
56
+ >(initialVerificationType || availableMethods?.[0] || 'totp');
57
+ const { request, getSettingValue, userEmail, showToastHandler } = useApp();
58
+ const t = useTranslations('core.RemoveMfaDialog');
59
+ const emailCodeLength = Number(getSettingValue('mfa-email-code-length')) || 6;
60
+
61
+ useEffect(() => {
62
+ if (open) {
63
+ let newMethod: 'totp' | 'email' | 'recovery' | 'webauthn' = 'recovery';
64
+
65
+ if (initialVerificationType) {
66
+ newMethod = initialVerificationType as
67
+ | 'totp'
68
+ | 'email'
69
+ | 'recovery'
70
+ | 'webauthn';
71
+ } else if (availableMethods && availableMethods.length > 0) {
72
+ newMethod = availableMethods[0] as
73
+ | 'totp'
74
+ | 'email'
75
+ | 'recovery'
76
+ | 'webauthn';
77
+ } else if (hasWebAuthn) {
78
+ newMethod = 'recovery';
79
+ }
80
+
81
+ setSelectedMethod(newMethod);
82
+ setCode('');
83
+ setResendCooldown(0);
84
+ }
85
+ }, [open, initialVerificationType, availableMethods, codeHash, hasWebAuthn]);
86
+
87
+ useEffect(() => {
88
+ if (resendCooldown > 0) {
89
+ const timer = setTimeout(() => {
90
+ setResendCooldown(resendCooldown - 1);
91
+ }, 1000);
92
+ return () => clearTimeout(timer);
93
+ }
94
+ }, [resendCooldown]);
95
+
96
+ const handleResendCode = async () => {
97
+ if (!userEmail) {
98
+ showToastHandler('error', t('emailNotFound'));
99
+ return;
100
+ }
101
+
102
+ setResendLoading(true);
103
+ try {
104
+ await request({
105
+ url: '/profile/mfa/check-verification-remove',
106
+ method: 'POST',
107
+ });
108
+ showToastHandler('success', t('codeResent'));
109
+ setResendCooldown(30);
110
+ } catch (error) {
111
+ console.error('Failed to resend code:', error);
112
+ showToastHandler('error', t('resendFailed'));
113
+ } finally {
114
+ setResendLoading(false);
115
+ }
116
+ };
117
+
118
+ const handleConfirm = async () => {
119
+ const minLength =
120
+ selectedMethod === 'recovery'
121
+ ? 1
122
+ : selectedMethod === 'email'
123
+ ? emailCodeLength
124
+ : 6;
125
+ if (code.length < minLength) return;
126
+
127
+ setLoading(true);
128
+ try {
129
+ const hashToSend = selectedMethod === 'email' ? codeHash : undefined;
130
+ await onConfirm(code, hashToSend, selectedMethod);
131
+ onOpenChange(false);
132
+ setCode('');
133
+ } catch (error) {
134
+ console.error('Verification failed:', error);
135
+ } finally {
136
+ setLoading(false);
137
+ }
138
+ };
139
+
140
+ const showMethodToggle =
141
+ (availableMethods && availableMethods.length > 0) || hasWebAuthn;
142
+
143
+ return (
144
+ <Dialog open={open} onOpenChange={onOpenChange}>
145
+ <DialogContent className="sm:max-w-[425px]">
146
+ <DialogHeader>
147
+ <DialogTitle className="flex items-center gap-2">
148
+ <AlertTriangle className="size-5 text-amber-500" />
149
+ {t('title')}
150
+ </DialogTitle>
151
+ <DialogDescription>
152
+ {t.rich('description', {
153
+ methodName: methodName as string,
154
+ strong: (chunks) => <strong>{chunks}</strong>,
155
+ })}
156
+ </DialogDescription>
157
+ </DialogHeader>
158
+
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
+ {showMethodToggle && (
167
+ <div className="flex gap-2 flex-wrap">
168
+ {availableMethods?.includes('totp') && (
169
+ <Button
170
+ type="button"
171
+ variant={selectedMethod === 'totp' ? 'default' : 'outline'}
172
+ size="sm"
173
+ className="flex-1"
174
+ onClick={() => setSelectedMethod('totp')}
175
+ >
176
+ <Key className="mr-2 size-4" />
177
+ {t('useAuthenticator')}
178
+ </Button>
179
+ )}
180
+ {availableMethods?.includes('email') && (
181
+ <Button
182
+ type="button"
183
+ variant={selectedMethod === 'email' ? 'default' : 'outline'}
184
+ size="sm"
185
+ className="flex-1"
186
+ onClick={() => setSelectedMethod('email')}
187
+ >
188
+ <Mail className="mr-2 size-4" />
189
+ {t('useEmail')}
190
+ </Button>
191
+ )}
192
+ {hasWebAuthn && (
193
+ <Button
194
+ type="button"
195
+ variant={
196
+ selectedMethod === 'webauthn' ? 'default' : 'outline'
197
+ }
198
+ size="sm"
199
+ className="flex-1"
200
+ onClick={async () => {
201
+ setLoading(true);
202
+ try {
203
+ const { startAuthentication } =
204
+ await import('@simplewebauthn/browser');
205
+
206
+ const { data: optionsData } = await request<any>({
207
+ url: '/profile/webauthn/authenticate/generate',
208
+ method: 'POST',
209
+ });
210
+
211
+ const assertion = await startAuthentication({
212
+ optionsJSON: optionsData,
213
+ });
214
+ await onConfirm('', undefined, 'webauthn', assertion);
215
+ setCode('');
216
+ setLoading(false);
217
+ onOpenChange(false);
218
+ } catch (error: any) {
219
+ console.error('WebAuthn verification failed:', error);
220
+ setLoading(false);
221
+ }
222
+ }}
223
+ disabled={loading}
224
+ >
225
+ <Fingerprint className="mr-2 size-4" />
226
+ {t('useWebAuthn')}
227
+ </Button>
228
+ )}
229
+ <Button
230
+ type="button"
231
+ variant={selectedMethod === 'recovery' ? 'default' : 'outline'}
232
+ size="sm"
233
+ className="flex-1"
234
+ onClick={() => setSelectedMethod('recovery')}
235
+ >
236
+ <Shield className="mr-2 size-4" />
237
+ {t('useRecoveryCode')}
238
+ </Button>
239
+ </div>
240
+ )}
241
+
242
+ <div className="space-y-2 flex flex-col justify-center items-center">
243
+ <Label htmlFor="verification-code" className="text-center">
244
+ {selectedMethod === 'email'
245
+ ? t('labelEmail')
246
+ : selectedMethod === 'recovery'
247
+ ? t('labelRecovery')
248
+ : selectedMethod === 'webauthn'
249
+ ? t('labelWebAuthn')
250
+ : t('labelTotp')}
251
+ </Label>
252
+ {selectedMethod === 'webauthn' ? (
253
+ <div className="text-center text-sm text-muted-foreground">
254
+ {t('webAuthnInstruction')}
255
+ </div>
256
+ ) : selectedMethod === 'recovery' ? (
257
+ <Input
258
+ id="verification-code"
259
+ value={code}
260
+ onChange={(e) => setCode(e.target.value)}
261
+ className="text-center text-lg font-mono uppercase"
262
+ autoComplete="off"
263
+ placeholder={t('placeholderRecovery')}
264
+ />
265
+ ) : (
266
+ <>
267
+ <div className="flex justify-center">
268
+ <InputOTP
269
+ maxLength={selectedMethod === 'email' ? emailCodeLength : 6}
270
+ value={code}
271
+ onChange={setCode}
272
+ id="verification-code"
273
+ >
274
+ <InputOTPGroup>
275
+ {Array.from({
276
+ length:
277
+ selectedMethod === 'email' ? emailCodeLength : 6,
278
+ }).map((_, i) => (
279
+ <InputOTPSlot key={i} index={i} />
280
+ ))}
281
+ </InputOTPGroup>
282
+ </InputOTP>
283
+ </div>
284
+ {selectedMethod === 'email' && (
285
+ <Button
286
+ type="button"
287
+ variant="ghost"
288
+ size="sm"
289
+ onClick={handleResendCode}
290
+ disabled={resendLoading || resendCooldown > 0}
291
+ className="mt-2"
292
+ >
293
+ {resendLoading
294
+ ? t('resending')
295
+ : resendCooldown > 0
296
+ ? t('resendIn', { seconds: resendCooldown })
297
+ : t('resendCode')}
298
+ </Button>
299
+ )}
300
+ </>
301
+ )}
302
+ </div>
303
+ </div>
304
+
305
+ <DialogFooter>
306
+ <Button
307
+ type="button"
308
+ variant="outline"
309
+ onClick={() => {
310
+ onOpenChange(false);
311
+ setCode('');
312
+ }}
313
+ disabled={loading}
314
+ >
315
+ {t('cancelButton')}
316
+ </Button>
317
+ {selectedMethod !== 'webauthn' && (
318
+ <Button
319
+ type="button"
320
+ variant="destructive"
321
+ onClick={handleConfirm}
322
+ disabled={
323
+ (selectedMethod === 'email' &&
324
+ code.length !== emailCodeLength) ||
325
+ (selectedMethod === 'totp' && code.length !== 6) ||
326
+ (selectedMethod === 'recovery' && !code) ||
327
+ loading
328
+ }
329
+ >
330
+ {loading ? t('removing') : t('removeButton')}
331
+ </Button>
332
+ )}
333
+ </DialogFooter>
334
+ </DialogContent>
335
+ </Dialog>
336
+ );
337
+ }