@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,461 @@
1
+ import { UserMfaTypeEnum } from '@hed-hog/api-types/UserMfaTypeEnum';
2
+ import { useApp } from '@hed-hog/next-app-provider';
3
+ import { useTranslations } from 'next-intl';
4
+ import { useEffect, useState } from 'react';
5
+
6
+ export function useMfaSetup() {
7
+ const { request, showToastHandler, user } = useApp();
8
+ const t = useTranslations('core.MfaSetupHook');
9
+ const [secret, setSecret] = useState('');
10
+ const [codeHash, setCodeHash] = useState('');
11
+ const [qrCode, setQrCode] = useState('');
12
+ const [challengeId, setChallengeId] = useState<number | null>(null);
13
+ const [resendLoading, setResendLoading] = useState(false);
14
+ const [resendCooldown, setResendCooldown] = useState(0);
15
+ const [currentEmail, setCurrentEmail] = useState<string | null>(null);
16
+
17
+ const userEmail = user?.user_identifier?.find(
18
+ (ui) => ui.type === 'email'
19
+ )?.value;
20
+
21
+ useEffect(() => {
22
+ if (resendCooldown > 0) {
23
+ const timer = setTimeout(
24
+ () => setResendCooldown(resendCooldown - 1),
25
+ 1000
26
+ );
27
+ return () => clearTimeout(timer);
28
+ }
29
+ }, [resendCooldown]);
30
+
31
+ const resendEmailCode = async () => {
32
+ if (!currentEmail) {
33
+ showToastHandler('error', t('emailNotFound'));
34
+ return;
35
+ }
36
+
37
+ setResendLoading(true);
38
+ try {
39
+ const { data }: any = await request<{ challengeId: number }>({
40
+ url: '/profile/mfa/email/verify',
41
+ method: 'POST',
42
+ data: { email: currentEmail },
43
+ });
44
+
45
+ setChallengeId(data.challengeId);
46
+ setResendCooldown(30);
47
+ showToastHandler('success', t('codeResent'));
48
+ } catch (error) {
49
+ showToastHandler('error', t('resendFailed'));
50
+ console.error(error);
51
+ } finally {
52
+ setResendLoading(false);
53
+ }
54
+ };
55
+
56
+ const sendCodeToRemoveEmailMFA = async () => {
57
+ try {
58
+ const { data }: any = await request({
59
+ url: `/profile/email/send-code-to-remove`,
60
+ method: 'POST',
61
+ data: { email: userEmail },
62
+ });
63
+
64
+ setCodeHash(data.codeHash);
65
+ showToastHandler('success', t('checkEmailSent'));
66
+ } catch (error) {
67
+ console.error(error);
68
+ throw error;
69
+ }
70
+ };
71
+
72
+ const enableEmailMFA = async (email: string) => {
73
+ try {
74
+ const { data }: any = await request<{ challengeId: number }>({
75
+ url: '/profile/mfa/email/verify',
76
+ method: 'POST',
77
+ data: { email },
78
+ });
79
+
80
+ setChallengeId(data.challengeId);
81
+ setCurrentEmail(email);
82
+ setResendCooldown(30);
83
+ showToastHandler('success', t('checkEmailSent'));
84
+ return data.challengeId;
85
+ } catch (error) {
86
+ console.error(error);
87
+ throw error;
88
+ }
89
+ };
90
+
91
+ const enableAuthenticatorMFA = async () => {
92
+ try {
93
+ const { data }: any = await request({
94
+ url: '/profile/totp/generate',
95
+ method: 'POST',
96
+ });
97
+
98
+ setSecret(data.secret);
99
+ setQrCode(data.qrCode);
100
+ showToastHandler('success', t('scanQrCode'));
101
+ } catch (error) {
102
+ console.error(error);
103
+ throw error;
104
+ }
105
+ };
106
+
107
+ const verifyEmailMFA = async (name: string, token: string, email: string) => {
108
+ try {
109
+ const { data }: any = await request<{ codes: string[] }>({
110
+ url: '/profile/mfa/email/verify/confirm',
111
+ method: 'POST',
112
+ data: { pin: token, challengeId, name, email },
113
+ });
114
+
115
+ showToastHandler('success', t('methodAddedSuccess'));
116
+ return data?.codes ? (data.codes as string[]) : [];
117
+ } catch (error) {
118
+ throw error;
119
+ }
120
+ };
121
+
122
+ const updateMFAName = async (mfaId: number, name: string) => {
123
+ try {
124
+ await request({
125
+ url: `/profile/update-mfa/${mfaId}`,
126
+ method: 'PUT',
127
+ data: { name },
128
+ });
129
+
130
+ showToastHandler('success', t('nameUpdatedSuccess'));
131
+ } catch (error) {
132
+ console.error(error);
133
+ }
134
+ };
135
+
136
+ const verifyTOTPMFA = async (name: string, verificationCode: string) => {
137
+ try {
138
+ const { data }: any = await request({
139
+ url: '/profile/totp/verify',
140
+ method: 'POST',
141
+ data: {
142
+ name,
143
+ token: verificationCode,
144
+ secret,
145
+ },
146
+ });
147
+
148
+ showToastHandler('success', t('methodAddedSuccess'));
149
+ return data.codes as string[];
150
+ } catch (error) {
151
+ throw error;
152
+ }
153
+ };
154
+
155
+ const handleRemoveTOTPMFA = async (methodId: number, token: string) => {
156
+ try {
157
+ await request({
158
+ url: `/profile/totp/${methodId}/remove`,
159
+ method: 'DELETE',
160
+ data: { token },
161
+ });
162
+
163
+ showToastHandler('success', t('methodRemovedSuccess'));
164
+ } catch (error) {
165
+ showToastHandler('error', t('removeMethodFailed'));
166
+ throw error;
167
+ }
168
+ };
169
+
170
+ const handleRemoveEmailMFA = async (methodId: number, token: string) => {
171
+ try {
172
+ await request({
173
+ url: `/profile/email/${methodId}/remove`,
174
+ method: 'DELETE',
175
+ data: { token, hash: codeHash },
176
+ });
177
+
178
+ showToastHandler('success', t('methodRemovedSuccess'));
179
+ } catch (error) {
180
+ throw error;
181
+ }
182
+ };
183
+
184
+ const removeMFAMethodWithRecoveryCode = async (
185
+ methodId: number,
186
+ recoveryCode: string
187
+ ) => {
188
+ try {
189
+ await request({
190
+ url: `/profile/mfa/${methodId}/remove-with-recovery-code`,
191
+ method: 'DELETE',
192
+ data: { recoveryCode },
193
+ });
194
+
195
+ showToastHandler('success', t('methodRemovedSuccess'));
196
+ } catch (error) {
197
+ showToastHandler('error', t('removeMethodFailed'));
198
+ throw error;
199
+ }
200
+ };
201
+
202
+ const handleRemoveWebAuthnMFA = async (methodId: number) => {
203
+ try {
204
+ const { startAuthentication } = await import('@simplewebauthn/browser');
205
+ const optionsResponse = await request<any>({
206
+ url: '/profile/webauthn/authenticate/generate',
207
+ method: 'POST',
208
+ });
209
+
210
+ if (!optionsResponse.data) {
211
+ throw new Error('Failed to generate authentication options');
212
+ }
213
+
214
+ showToastHandler('success', t('authenticateToRemove'));
215
+ const assertion = await startAuthentication(optionsResponse.data);
216
+ await request({
217
+ url: '/profile/webauthn/authenticate/verify',
218
+ method: 'POST',
219
+ data: {
220
+ assertionResponse: assertion,
221
+ },
222
+ });
223
+
224
+ await request({
225
+ url: `/profile/webauthn/${methodId}/remove`,
226
+ method: 'DELETE',
227
+ });
228
+ showToastHandler('success', t('securityKeyRemovedSuccess'));
229
+ } catch (error: any) {
230
+ if (error?.name === 'NotAllowedError') {
231
+ showToastHandler('error', t('authenticationCancelled'));
232
+ } else {
233
+ showToastHandler(
234
+ 'error',
235
+ error?.message || t('removeSecurityKeyFailed')
236
+ );
237
+ }
238
+ throw error;
239
+ }
240
+ };
241
+
242
+ const removeMFAMethod = async (
243
+ methodId: number,
244
+ methodType: UserMfaTypeEnum,
245
+ verificationCode: string
246
+ ) => {
247
+ switch (methodType) {
248
+ case UserMfaTypeEnum.TOTP:
249
+ await handleRemoveTOTPMFA(methodId, verificationCode);
250
+ break;
251
+ case UserMfaTypeEnum.WEBAUTHN:
252
+ await handleRemoveWebAuthnMFA(methodId);
253
+ break;
254
+ case UserMfaTypeEnum.EMAIL:
255
+ await handleRemoveEmailMFA(methodId, verificationCode);
256
+ break;
257
+
258
+ default:
259
+ await handleRemoveEmailMFA(methodId, verificationCode);
260
+ }
261
+ };
262
+
263
+ const removeMfaUnified = async (
264
+ methodId: number,
265
+ token?: string,
266
+ hash?: string,
267
+ verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn',
268
+ assertionResponse?: any
269
+ ) => {
270
+ try {
271
+ await request({
272
+ url: `/profile/mfa/${methodId}/remove`,
273
+ method: 'DELETE',
274
+ data: { token, hash, verificationType, assertionResponse },
275
+ });
276
+
277
+ showToastHandler('success', t('methodRemovedSuccess'));
278
+ } catch (error) {
279
+ showToastHandler('error', t('removeMethodFailed'));
280
+ throw error;
281
+ }
282
+ };
283
+
284
+ const initiateSetup = async (type: UserMfaTypeEnum, email?: string) => {
285
+ switch (type) {
286
+ case UserMfaTypeEnum.EMAIL:
287
+ if (email) {
288
+ await enableEmailMFA(email);
289
+ } else {
290
+ throw new Error('Email is required for EMAIL MFA');
291
+ }
292
+ break;
293
+ case UserMfaTypeEnum.TOTP:
294
+ await enableAuthenticatorMFA();
295
+ break;
296
+ default:
297
+ if (email) {
298
+ await enableEmailMFA(email);
299
+ } else {
300
+ throw new Error('Email is required for EMAIL MFA');
301
+ }
302
+ }
303
+ };
304
+
305
+ const checkMfaVerificationType = async () => {
306
+ try {
307
+ const { data }: any = await request({
308
+ url: '/profile/recovery-codes/send-verification',
309
+ method: 'POST',
310
+ });
311
+
312
+ return {
313
+ requiresVerification: data.requiresVerification,
314
+ verificationType: data.verificationType,
315
+ availableMethods: data.availableMethods,
316
+ codeHash: data.codeHash,
317
+ hasWebAuthn: data.hasWebAuthn,
318
+ hasRecoveryCodes: data.hasRecoveryCodes,
319
+ };
320
+ } catch (error) {
321
+ showToastHandler('error', t('checkVerificationTypeFailed'));
322
+ throw error;
323
+ }
324
+ };
325
+
326
+ const regenerateRecoveryCodes = async (
327
+ verificationCode?: string,
328
+ hash?: string,
329
+ verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn',
330
+ assertionResponse?: any
331
+ ) => {
332
+ try {
333
+ const { data }: any = await request({
334
+ url: '/profile/recovery-codes/regenerate',
335
+ method: 'POST',
336
+ data: { verificationCode, hash, verificationType, assertionResponse },
337
+ });
338
+
339
+ showToastHandler('success', t('recoveryCodesRegeneratedSuccess'));
340
+ return data.codes as string[];
341
+ } catch (error) {
342
+ showToastHandler('error', t('regenerateRecoveryCodesFailed'));
343
+ throw error;
344
+ }
345
+ };
346
+
347
+ const checkIfMfaExists = async () => {
348
+ try {
349
+ const { data }: any = await request({
350
+ url: '/profile/mfa/check-verification',
351
+ method: 'POST',
352
+ });
353
+
354
+ return {
355
+ requiresVerification: data.requiresVerification,
356
+ verificationType: data.verificationType,
357
+ availableMethods: data.availableMethods,
358
+ codeHash: data.codeHash,
359
+ hasWebAuthn: data.hasWebAuthn,
360
+ hasRecoveryCodes: data.hasRecoveryCodes,
361
+ };
362
+ } catch (error) {
363
+ showToastHandler('error', t('checkExistingMfaFailed'));
364
+ throw error;
365
+ }
366
+ };
367
+
368
+ const checkMfaBeforeRemove = async () => {
369
+ try {
370
+ const { data }: any = await request({
371
+ url: '/profile/mfa/check-verification-remove',
372
+ method: 'POST',
373
+ });
374
+
375
+ return {
376
+ requiresVerification: data.requiresVerification,
377
+ verificationType: data.verificationType,
378
+ availableMethods: data.availableMethods,
379
+ codeHash: data.codeHash,
380
+ hasWebAuthn: data.hasWebAuthn,
381
+ hasRecoveryCodes: data.hasRecoveryCodes,
382
+ };
383
+ } catch (error) {
384
+ showToastHandler('error', t('checkMfaBeforeRemoveFailed'));
385
+ throw error;
386
+ }
387
+ };
388
+
389
+ const verifyBeforeAddMfa = async (
390
+ verificationCode: string,
391
+ hash?: string,
392
+ verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn',
393
+ assertionResponse?: any
394
+ ) => {
395
+ try {
396
+ await request({
397
+ url: '/profile/mfa/verify-before-add',
398
+ method: 'POST',
399
+ data: { verificationCode, hash, verificationType, assertionResponse },
400
+ });
401
+
402
+ return true;
403
+ } catch (error) {
404
+ console.error('❌ verifyBeforeAddMfa error:', error);
405
+ showToastHandler('error', t('invalidVerificationCode'));
406
+ throw error;
407
+ }
408
+ };
409
+
410
+ const enableWebAuthnMFA = async (name: string) => {
411
+ try {
412
+ const { startRegistration } = await import('@simplewebauthn/browser');
413
+
414
+ const { data: options }: any = await request({
415
+ url: '/profile/webauthn/generate',
416
+ method: 'POST',
417
+ data: { name },
418
+ });
419
+
420
+ const attestationResponse = await startRegistration(options);
421
+
422
+ const { data }: any = await request({
423
+ url: '/profile/webauthn/verify',
424
+ method: 'POST',
425
+ data: { name, attestationResponse },
426
+ });
427
+
428
+ showToastHandler('success', t('securityKeyRegisteredSuccess'));
429
+ return data.codes as string[];
430
+ } catch (error: any) {
431
+ if (error.name === 'NotAllowedError') {
432
+ showToastHandler('error', t('registrationCancelled'));
433
+ } else {
434
+ showToastHandler('error', t('registerSecurityKeyFailed'));
435
+ }
436
+ throw error;
437
+ }
438
+ };
439
+
440
+ return {
441
+ qrCode,
442
+ userEmail,
443
+ verifyEmailMFA,
444
+ verifyTOTPMFA,
445
+ removeMFAMethod,
446
+ removeMfaUnified,
447
+ removeMFAMethodWithRecoveryCode,
448
+ updateMFAName,
449
+ sendCodeToRemoveEmailMFA,
450
+ initiateSetup,
451
+ regenerateRecoveryCodes,
452
+ checkMfaVerificationType,
453
+ checkIfMfaExists,
454
+ checkMfaBeforeRemove,
455
+ verifyBeforeAddMfa,
456
+ enableWebAuthnMFA,
457
+ resendEmailCode,
458
+ resendLoading,
459
+ resendCooldown,
460
+ };
461
+ }
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import { PageHeader } from '@/components/entity-list';
4
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipTrigger,
9
+ } from '@/components/ui/tooltip';
10
+ import {
11
+ Link,
12
+ Lock,
13
+ LucideIcon,
14
+ Mail,
15
+ Shield,
16
+ Smartphone,
17
+ User,
18
+ } from 'lucide-react';
19
+ import { useTranslations } from 'next-intl';
20
+ import { usePathname, useRouter } from 'next/navigation';
21
+ import { ReactNode } from 'react';
22
+
23
+ type LayoutProps = {
24
+ children: ReactNode;
25
+ };
26
+
27
+ type TabConfig = {
28
+ value: string;
29
+ label: string;
30
+ icon: LucideIcon;
31
+ };
32
+
33
+ export default function AccountLayout({ children }: LayoutProps) {
34
+ const pathname = usePathname();
35
+ const router = useRouter();
36
+ const t = useTranslations('core.Profile');
37
+
38
+ const tabs: TabConfig[] = [
39
+ { value: 'profile', label: t('tabProfile'), icon: User },
40
+ { value: 'password', label: t('tabPassword'), icon: Lock },
41
+ { value: 'email', label: t('tabEmail'), icon: Mail },
42
+ { value: '2fa', label: t('tab2fa'), icon: Shield },
43
+ { value: 'accounts', label: t('tabAccounts'), icon: Link },
44
+ { value: 'sessions', label: t('tabSessions'), icon: Smartphone },
45
+ ];
46
+
47
+ const getCurrentTab = () => {
48
+ if (pathname.includes('/profile')) return 'profile';
49
+ if (pathname.includes('/password')) return 'password';
50
+ if (pathname.includes('/email')) return 'email';
51
+ if (pathname.includes('/2fa')) return '2fa';
52
+ if (pathname.includes('/accounts')) return 'accounts';
53
+ if (pathname.includes('/sessions')) return 'sessions';
54
+ return 'profile';
55
+ };
56
+
57
+ const handleTabChange = (value: string) => {
58
+ router.push(`/account/${value}`);
59
+ };
60
+
61
+ return (
62
+ <div className="flex flex-col pl-4 h-screen">
63
+ <PageHeader
64
+ breadcrumbs={[
65
+ { label: 'Home', href: '/' },
66
+ { label: t('breadcrumbLabel') },
67
+ ]}
68
+ title={t('title')}
69
+ description={t('description')}
70
+ />
71
+ <div className="py-2">
72
+ <Tabs value={getCurrentTab()} onValueChange={handleTabChange}>
73
+ <TabsList className="grid w-full grid-cols-6 lg:w-fit gap-1 rounded-sm">
74
+ {tabs.map((tab) => {
75
+ const Icon = tab.icon;
76
+ const isActive = getCurrentTab() === tab.value;
77
+
78
+ return (
79
+ <Tooltip key={tab.value}>
80
+ <TooltipTrigger asChild>
81
+ <TabsTrigger
82
+ value={tab.value}
83
+ className={`gap-2 cursor-pointer hover:bg-primary/10 active:bg-primary/20 rounded-sm ${isActive ? 'text-primary' : 'text-primary/50'}`}
84
+ >
85
+ <Icon className="size-4" />
86
+ <span
87
+ className={`hidden sm:inline ${isActive ? 'text-primary' : 'text-primary/50'}`}
88
+ >
89
+ {tab.label}
90
+ </span>
91
+ </TabsTrigger>
92
+ </TooltipTrigger>
93
+ <TooltipContent side="bottom" className="block sm:hidden">
94
+ {tab.label}
95
+ </TooltipContent>
96
+ </Tooltip>
97
+ );
98
+ })}
99
+ </TabsList>
100
+ <div className="mt-2">{children}</div>
101
+ </Tabs>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,37 @@
1
+ import { UserMfaTypeEnum } from '@hed-hog/api-types/UserMfaTypeEnum';
2
+ import { Fingerprint, Key, LucideIcon, Mail, Shield } from 'lucide-react';
3
+
4
+ export function getMethodIcon(type: string): React.ReactElement<LucideIcon> {
5
+ switch (type) {
6
+ case UserMfaTypeEnum.EMAIL:
7
+ return <Mail className="size-5" />;
8
+ case UserMfaTypeEnum.TOTP:
9
+ return <Key className="size-5" />;
10
+ case UserMfaTypeEnum.WEBAUTHN:
11
+ return <Fingerprint className="size-5" />;
12
+ default:
13
+ return <Shield className="size-5" />;
14
+ }
15
+ }
16
+
17
+ export function getMethodName(type: string): string {
18
+ switch (type) {
19
+ case UserMfaTypeEnum.EMAIL:
20
+ return 'Email';
21
+ case UserMfaTypeEnum.TOTP:
22
+ return 'Authenticator App';
23
+ case UserMfaTypeEnum.WEBAUTHN:
24
+ return 'Security Key';
25
+ default:
26
+ return type;
27
+ }
28
+ }
29
+
30
+ export async function copyToClipboard(text: string): Promise<boolean> {
31
+ try {
32
+ await navigator.clipboard.writeText(text);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation';
2
+
3
+ export default function Page() {
4
+ redirect('/account/profile');
5
+ }
@@ -0,0 +1,5 @@
1
+ import { ChangePasswordForm } from '../components/change-password-form';
2
+
3
+ export default function PasswordPage() {
4
+ return <ChangePasswordForm />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { ProfileForm } from '../components/profile-form';
2
+
3
+ export default function ProfilePage() {
4
+ return <ProfileForm />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { ActiveSessions } from '../components/active-sessions';
2
+
3
+ export default function SessionsPage() {
4
+ return <ActiveSessions />;
5
+ }