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