@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.
- package/hedhog/frontend/app/account/2fa/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/accounts/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +356 -0
- package/hedhog/frontend/app/account/components/change-email-form.tsx.ejs +379 -0
- package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +184 -0
- package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +144 -0
- package/hedhog/frontend/app/account/components/email-request-dialog.tsx.ejs +96 -0
- package/hedhog/frontend/app/account/components/mfa-add-buttons.tsx.ejs +43 -0
- package/hedhog/frontend/app/account/components/mfa-method-card.tsx.ejs +115 -0
- package/hedhog/frontend/app/account/components/mfa-setup-dialog.tsx.ejs +236 -0
- package/hedhog/frontend/app/account/components/profile-form.tsx.ejs +209 -0
- package/hedhog/frontend/app/account/components/recovery-codes-dialog.tsx.ejs +192 -0
- package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +372 -0
- package/hedhog/frontend/app/account/components/remove-mfa-dialog.tsx.ejs +337 -0
- package/hedhog/frontend/app/account/components/two-factor-auth.tsx.ejs +393 -0
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +332 -0
- package/hedhog/frontend/app/account/email/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/hooks/use-mfa-methods.ts.ejs +27 -0
- package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +461 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +105 -0
- package/hedhog/frontend/app/account/lib/mfa-utils.tsx.ejs +37 -0
- package/hedhog/frontend/app/account/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/password/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/profile/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/account/sessions/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +490 -0
- package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +62 -0
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +316 -0
- package/hedhog/frontend/app/configurations/page.tsx.ejs +35 -0
- package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +351 -0
- package/hedhog/frontend/app/dashboard/[slug]/page.tsx.ejs +11 -0
- package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +62 -0
- package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +45 -0
- package/hedhog/frontend/app/dashboard/dashboard.css.ejs +196 -0
- package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +63 -0
- package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +516 -0
- package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +753 -0
- package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +516 -0
- package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +489 -0
- package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +621 -0
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -0
- package/hedhog/frontend/app/mail/log/page.tsx.ejs +312 -0
- package/hedhog/frontend/app/mail/template/page.tsx.ejs +1177 -0
- package/hedhog/frontend/app/preferences/page.tsx.ejs +448 -0
- package/hedhog/frontend/app/roles/menus.tsx.ejs +504 -0
- package/hedhog/frontend/app/roles/page.tsx.ejs +814 -0
- package/hedhog/frontend/app/roles/routes.tsx.ejs +397 -0
- package/hedhog/frontend/app/roles/users.tsx.ejs +306 -0
- package/hedhog/frontend/app/users/active-session.tsx.ejs +159 -0
- package/hedhog/frontend/app/users/identifiers.tsx.ejs +279 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +1257 -0
- package/hedhog/frontend/app/users/permissions.tsx.ejs +155 -0
- package/hedhog/frontend/messages/en.json +1080 -0
- package/hedhog/frontend/messages/pt.json +1135 -0
- 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
|
+
}
|