@striae-org/striae 5.3.2 → 5.4.0
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/app/components/auth/auth.module.css +531 -0
- package/app/components/auth/mfa-enrollment.tsx +132 -79
- package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
- package/app/components/auth/mfa-verification.tsx +155 -33
- package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
- package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
- package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
- package/app/components/navbar/navbar.tsx +1 -1
- package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
- package/app/components/sidebar/files/files-modal.module.css +29 -0
- package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
- package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
- package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
- package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
- package/app/components/sidebar/notes/notes.module.css +52 -0
- package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
- package/app/components/toolbar/toolbar.module.css +181 -2
- package/app/components/user/delete-account.tsx +7 -7
- package/app/components/user/inactivity-warning.tsx +6 -6
- package/app/components/user/manage-profile.tsx +18 -1
- package/app/components/user/mfa-enrolled-factors.tsx +117 -0
- package/app/components/user/mfa-phone-update.tsx +8 -4
- package/app/components/user/mfa-totp-section.tsx +446 -0
- package/app/components/user/user.module.css +665 -0
- package/app/routes/striae/striae.tsx +1 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
- package/app/services/firebase/errors.ts +2 -0
- package/app/utils/auth/mfa.ts +35 -1
- package/package.json +16 -21
- package/scripts/deploy-all.sh +166 -0
- package/scripts/deploy-config/modules/env-utils.sh +322 -0
- package/scripts/deploy-config/modules/keys.sh +404 -0
- package/scripts/deploy-config/modules/prompt.sh +375 -0
- package/scripts/deploy-config/modules/scaffolding.sh +310 -0
- package/scripts/deploy-config/modules/validation.sh +354 -0
- package/scripts/deploy-config.sh +236 -0
- package/scripts/deploy-pages-secrets.sh +231 -0
- package/scripts/deploy-pages.sh +34 -0
- package/scripts/deploy-primershear-emails.sh +167 -0
- package/scripts/deploy-worker-secrets.sh +385 -0
- package/scripts/dev.cjs +23 -0
- package/scripts/enable-totp-mfa.mjs +57 -0
- package/scripts/install-workers.sh +87 -0
- package/scripts/run-eslint.cjs +43 -0
- package/scripts/update-compatibility-dates.cjs +124 -0
- package/scripts/update-markdown-versions.cjs +43 -0
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/auth/mfa-enrollment.module.css +0 -276
- package/app/components/auth/mfa-verification.module.css +0 -259
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
- package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
- package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
- package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
- package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
- package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
- package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
- package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
- package/app/components/user/delete-account.module.css +0 -277
- package/app/components/user/inactivity-warning.module.css +0 -148
- package/app/components/user/manage-profile.module.css +0 -192
- package/app/routes/auth/login.module.css +0 -523
- package/app/routes/auth/login.tsx +0 -705
- /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
- /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
- /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
- /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
- /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
- /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
import { handleAuthError, getValidationError } from '~/services/firebase/errors';
|
|
12
12
|
import { SignOut } from '~/components/actions/signout';
|
|
13
13
|
import { auditService } from '~/services/audit';
|
|
14
|
-
import
|
|
14
|
+
import { MfaTotpEnrollment } from './mfa-totp-enrollment';
|
|
15
|
+
import styles from './auth.module.css';
|
|
15
16
|
|
|
16
17
|
interface MFAEnrollmentProps {
|
|
17
18
|
user: User;
|
|
@@ -28,6 +29,7 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
|
|
|
28
29
|
onSkip,
|
|
29
30
|
mandatory = true
|
|
30
31
|
}) => {
|
|
32
|
+
const [enrollmentMethod, setEnrollmentMethod] = useState<'choice' | 'sms' | 'totp'>('choice');
|
|
31
33
|
const [phoneNumber, setPhoneNumber] = useState('');
|
|
32
34
|
const [verificationCode, setVerificationCode] = useState('');
|
|
33
35
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -273,12 +275,12 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
|
|
|
273
275
|
|
|
274
276
|
return (
|
|
275
277
|
<div className={styles.overlay}>
|
|
276
|
-
<div className={styles.
|
|
278
|
+
<div className={styles.enrollmentModal}>
|
|
277
279
|
<div className={styles.header}>
|
|
278
280
|
<h2>Security Setup Required</h2>
|
|
279
281
|
<p>
|
|
280
282
|
{mandatory
|
|
281
|
-
? 'Two-factor authentication is required for all accounts. Please set up
|
|
283
|
+
? 'Two-factor authentication is required for all accounts. Please set up a second factor to continue.'
|
|
282
284
|
: 'Enhance your account security with two-factor authentication.'
|
|
283
285
|
}
|
|
284
286
|
</p>
|
|
@@ -290,91 +292,142 @@ export const MFAEnrollment: React.FC<MFAEnrollmentProps> = ({
|
|
|
290
292
|
{errorMessage}
|
|
291
293
|
</div>
|
|
292
294
|
)}
|
|
293
|
-
|
|
294
|
-
{
|
|
295
|
-
<div className={styles.
|
|
296
|
-
<h3>
|
|
297
|
-
<
|
|
298
|
-
type="
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
className={styles.input}
|
|
306
|
-
disabled={isLoading}
|
|
307
|
-
/>
|
|
308
|
-
<p className={styles.note}>
|
|
309
|
-
We'll send a verification code to this number.
|
|
310
|
-
</p>
|
|
295
|
+
|
|
296
|
+
{enrollmentMethod === 'choice' && (
|
|
297
|
+
<div className={styles.methodChoice}>
|
|
298
|
+
<h3>Choose a Verification Method</h3>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
onClick={() => setEnrollmentMethod('sms')}
|
|
302
|
+
className={styles.methodButton}
|
|
303
|
+
>
|
|
304
|
+
<span className={styles.methodButtonTitle}>Text Message (SMS)</span>
|
|
305
|
+
<span className={styles.methodButtonDesc}>Receive a code by text message</span>
|
|
306
|
+
</button>
|
|
311
307
|
<button
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
className={styles.
|
|
308
|
+
type="button"
|
|
309
|
+
onClick={() => setEnrollmentMethod('totp')}
|
|
310
|
+
className={styles.methodButton}
|
|
315
311
|
>
|
|
316
|
-
{
|
|
312
|
+
<span className={styles.methodButtonTitle}>Authenticator App</span>
|
|
313
|
+
<span className={styles.methodButtonDesc}>Use Google Authenticator, Authy, or similar</span>
|
|
317
314
|
</button>
|
|
318
315
|
</div>
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
<button
|
|
348
|
-
onClick={() => {
|
|
349
|
-
setCodeSent(false);
|
|
350
|
-
setVerificationCode('');
|
|
351
|
-
setErrorMessage(''); // Clear errors when changing phone number
|
|
352
|
-
}}
|
|
353
|
-
disabled={isLoading}
|
|
354
|
-
className={styles.secondaryButton}
|
|
355
|
-
>
|
|
356
|
-
Change Phone Number
|
|
357
|
-
</button>
|
|
358
|
-
|
|
359
|
-
{resendTimer === 0 ? (
|
|
360
|
-
<button
|
|
361
|
-
onClick={sendVerificationCode}
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{enrollmentMethod === 'totp' && (
|
|
319
|
+
<MfaTotpEnrollment
|
|
320
|
+
user={user}
|
|
321
|
+
onSuccess={onSuccess}
|
|
322
|
+
onError={onError}
|
|
323
|
+
onBack={() => {
|
|
324
|
+
setEnrollmentMethod('choice');
|
|
325
|
+
setErrorMessage('');
|
|
326
|
+
}}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{enrollmentMethod === 'sms' && (
|
|
331
|
+
<>
|
|
332
|
+
{!codeSent ? (
|
|
333
|
+
<div className={styles.phoneStep}>
|
|
334
|
+
<h3>Step 1: Enter Your Mobile Number</h3>
|
|
335
|
+
<input
|
|
336
|
+
type="tel"
|
|
337
|
+
value={phoneNumber}
|
|
338
|
+
onChange={(e) => {
|
|
339
|
+
setPhoneNumber(e.target.value);
|
|
340
|
+
if (errorMessage) setErrorMessage(''); // Clear error on input
|
|
341
|
+
}}
|
|
342
|
+
placeholder="ex. +15551234567"
|
|
343
|
+
className={styles.enrollmentInput}
|
|
362
344
|
disabled={isLoading}
|
|
363
|
-
|
|
364
|
-
>
|
|
365
|
-
|
|
366
|
-
</button>
|
|
367
|
-
) : (
|
|
368
|
-
<p className={styles.resendTimer}>
|
|
369
|
-
Resend code in {resendTimer}s
|
|
345
|
+
/>
|
|
346
|
+
<p className={styles.note}>
|
|
347
|
+
We'll send a verification code to this number.
|
|
370
348
|
</p>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
349
|
+
<div className={styles.buttonGroup}>
|
|
350
|
+
<button
|
|
351
|
+
onClick={sendVerificationCode}
|
|
352
|
+
disabled={isLoading || !phoneNumber.trim()}
|
|
353
|
+
className={styles.primaryButton}
|
|
354
|
+
>
|
|
355
|
+
{isLoading ? 'Sending...' : 'Send Verification Code'}
|
|
356
|
+
</button>
|
|
357
|
+
<button
|
|
358
|
+
type="button"
|
|
359
|
+
onClick={() => {
|
|
360
|
+
setEnrollmentMethod('choice');
|
|
361
|
+
setErrorMessage('');
|
|
362
|
+
}}
|
|
363
|
+
disabled={isLoading}
|
|
364
|
+
className={styles.enrollmentSecondaryButton}
|
|
365
|
+
>
|
|
366
|
+
Back
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
) : (
|
|
371
|
+
<div className={styles.codeStep}>
|
|
372
|
+
<h3>Step 2: Enter Verification Code</h3>
|
|
373
|
+
<p className={styles.note}>
|
|
374
|
+
Enter the 6-digit code sent to {phoneNumber}
|
|
375
|
+
</p>
|
|
376
|
+
<input
|
|
377
|
+
type="text"
|
|
378
|
+
value={verificationCode}
|
|
379
|
+
onChange={(e) => {
|
|
380
|
+
setVerificationCode(e.target.value.replace(/\D/g, ''));
|
|
381
|
+
if (errorMessage) setErrorMessage(''); // Clear error on input
|
|
382
|
+
}}
|
|
383
|
+
placeholder="123456"
|
|
384
|
+
maxLength={6}
|
|
385
|
+
className={styles.enrollmentInput}
|
|
386
|
+
disabled={isLoading}
|
|
387
|
+
/>
|
|
388
|
+
|
|
389
|
+
<div className={styles.buttonGroup}>
|
|
390
|
+
<button
|
|
391
|
+
onClick={enrollMFA}
|
|
392
|
+
disabled={isLoading || verificationCode.length !== 6}
|
|
393
|
+
className={styles.primaryButton}
|
|
394
|
+
>
|
|
395
|
+
{isLoading ? 'Verifying...' : 'Complete Setup'}
|
|
396
|
+
</button>
|
|
397
|
+
|
|
398
|
+
<button
|
|
399
|
+
onClick={() => {
|
|
400
|
+
setCodeSent(false);
|
|
401
|
+
setVerificationCode('');
|
|
402
|
+
setErrorMessage(''); // Clear errors when changing phone number
|
|
403
|
+
}}
|
|
404
|
+
disabled={isLoading}
|
|
405
|
+
className={styles.enrollmentSecondaryButton}
|
|
406
|
+
>
|
|
407
|
+
Change Phone Number
|
|
408
|
+
</button>
|
|
409
|
+
|
|
410
|
+
{resendTimer === 0 ? (
|
|
411
|
+
<button
|
|
412
|
+
onClick={sendVerificationCode}
|
|
413
|
+
disabled={isLoading}
|
|
414
|
+
className={styles.enrollmentSecondaryButton}
|
|
415
|
+
>
|
|
416
|
+
Resend Code
|
|
417
|
+
</button>
|
|
418
|
+
) : (
|
|
419
|
+
<p className={styles.resendTimer}>
|
|
420
|
+
Resend code in {resendTimer}s
|
|
421
|
+
</p>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
</>
|
|
374
427
|
)}
|
|
375
428
|
</div>
|
|
376
429
|
|
|
377
|
-
{!mandatory && (
|
|
430
|
+
{!mandatory && enrollmentMethod === 'choice' && (
|
|
378
431
|
<div className={styles.footer}>
|
|
379
432
|
<button
|
|
380
433
|
onClick={handleSkip}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { multiFactor, TotpMultiFactorGenerator, type TotpSecret, type User } from 'firebase/auth';
|
|
4
|
+
import { toDataURL as qrToDataURL } from 'qrcode';
|
|
5
|
+
import { handleAuthError, getValidationError } from '~/services/firebase/errors';
|
|
6
|
+
import { auditService } from '~/services/audit';
|
|
7
|
+
import styles from './auth.module.css';
|
|
8
|
+
|
|
9
|
+
interface MfaTotpEnrollmentProps {
|
|
10
|
+
user: User;
|
|
11
|
+
onSuccess: () => void;
|
|
12
|
+
onError: (error: string) => void;
|
|
13
|
+
onBack?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MfaTotpEnrollment: React.FC<MfaTotpEnrollmentProps> = ({
|
|
17
|
+
user,
|
|
18
|
+
onSuccess,
|
|
19
|
+
onError,
|
|
20
|
+
onBack,
|
|
21
|
+
}) => {
|
|
22
|
+
const [totpSecret, setTotpSecret] = useState<TotpSecret | null>(null);
|
|
23
|
+
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('');
|
|
24
|
+
const [verificationCode, setVerificationCode] = useState('');
|
|
25
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
26
|
+
const [isFetchingSecret, setIsFetchingSecret] = useState(true);
|
|
27
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
28
|
+
const [showSecretKey, setShowSecretKey] = useState(false);
|
|
29
|
+
// Persists the secret across re-renders to avoid regenerating it
|
|
30
|
+
const secretRef = useRef<TotpSecret | null>(null);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (secretRef.current) {
|
|
34
|
+
setTotpSecret(secretRef.current);
|
|
35
|
+
setIsFetchingSecret(false);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generateSecret = async () => {
|
|
40
|
+
setIsFetchingSecret(true);
|
|
41
|
+
setErrorMessage('');
|
|
42
|
+
try {
|
|
43
|
+
const session = await multiFactor(user).getSession();
|
|
44
|
+
const secret = await TotpMultiFactorGenerator.generateSecret(session);
|
|
45
|
+
secretRef.current = secret;
|
|
46
|
+
setTotpSecret(secret);
|
|
47
|
+
|
|
48
|
+
const qrUrl = secret.generateQrCodeUrl(user.email ?? 'user', 'Striae');
|
|
49
|
+
const dataUrl = await qrToDataURL(qrUrl, { width: 200, margin: 1 });
|
|
50
|
+
setQrCodeDataUrl(dataUrl);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const { message } = handleAuthError(err);
|
|
53
|
+
setErrorMessage(message);
|
|
54
|
+
onError(message);
|
|
55
|
+
} finally {
|
|
56
|
+
setIsFetchingSecret(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
void generateSecret();
|
|
61
|
+
}, [user, onError]);
|
|
62
|
+
|
|
63
|
+
const handleVerify = async () => {
|
|
64
|
+
if (!totpSecret) {
|
|
65
|
+
const error = getValidationError('MFA_TOTP_SETUP_ERROR');
|
|
66
|
+
setErrorMessage(error);
|
|
67
|
+
onError(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (verificationCode.length !== 6) {
|
|
72
|
+
const error = getValidationError('MFA_CODE_REQUIRED');
|
|
73
|
+
setErrorMessage(error);
|
|
74
|
+
onError(error);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setIsLoading(true);
|
|
79
|
+
setErrorMessage('');
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const assertion = TotpMultiFactorGenerator.assertionForEnrollment(
|
|
83
|
+
totpSecret,
|
|
84
|
+
verificationCode
|
|
85
|
+
);
|
|
86
|
+
await multiFactor(user).enroll(assertion, 'Authenticator App');
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await auditService.logMfaEnrollment(
|
|
90
|
+
user,
|
|
91
|
+
undefined,
|
|
92
|
+
'totp',
|
|
93
|
+
'success',
|
|
94
|
+
1,
|
|
95
|
+
undefined,
|
|
96
|
+
navigator.userAgent
|
|
97
|
+
);
|
|
98
|
+
} catch (auditErr) {
|
|
99
|
+
console.error('Failed to log TOTP enrollment audit:', auditErr);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
onSuccess();
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const authError = err as { code?: string; message?: string };
|
|
105
|
+
let errorMsg = '';
|
|
106
|
+
|
|
107
|
+
if (authError.code === 'auth/invalid-verification-code') {
|
|
108
|
+
errorMsg = getValidationError('MFA_INVALID_CODE');
|
|
109
|
+
} else if (authError.code === 'auth/code-expired') {
|
|
110
|
+
errorMsg = getValidationError('MFA_CODE_EXPIRED');
|
|
111
|
+
} else {
|
|
112
|
+
errorMsg = handleAuthError(authError).message;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setErrorMessage(errorMsg);
|
|
116
|
+
onError(errorMsg);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await auditService.logSecurityViolation(
|
|
120
|
+
user,
|
|
121
|
+
'unauthorized-access',
|
|
122
|
+
authError.code === 'auth/invalid-verification-code' ? 'high' : 'medium',
|
|
123
|
+
`Failed TOTP enrollment: ${authError.code} - ${errorMsg}`,
|
|
124
|
+
'mfa-totp-enrollment-endpoint',
|
|
125
|
+
true
|
|
126
|
+
);
|
|
127
|
+
} catch (auditErr) {
|
|
128
|
+
console.error('Failed to log TOTP enrollment security violation audit:', auditErr);
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
setIsLoading(false);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (isFetchingSecret) {
|
|
136
|
+
return (
|
|
137
|
+
<div className={styles.totpStep}>
|
|
138
|
+
<p className={styles.note}>Setting up authenticator…</p>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className={styles.totpStep}>
|
|
145
|
+
<h3>Set Up Authenticator App</h3>
|
|
146
|
+
|
|
147
|
+
{errorMessage && (
|
|
148
|
+
<div className={styles.errorMessage}>{errorMessage}</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{qrCodeDataUrl && (
|
|
152
|
+
<div className={styles.qrCodeContainer}>
|
|
153
|
+
<p className={styles.note}>
|
|
154
|
+
Scan this QR code with your authenticator app (Google Authenticator, Authy, etc.).
|
|
155
|
+
</p>
|
|
156
|
+
<img
|
|
157
|
+
src={qrCodeDataUrl}
|
|
158
|
+
alt="TOTP QR Code — scan with your authenticator app"
|
|
159
|
+
className={styles.qrCodeImage}
|
|
160
|
+
width={200}
|
|
161
|
+
height={200}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => setShowSecretKey((v) => !v)}
|
|
169
|
+
className={styles.enrollmentSecondaryButton}
|
|
170
|
+
style={{ marginBottom: '1rem' }}
|
|
171
|
+
>
|
|
172
|
+
{showSecretKey ? 'Hide' : 'Show'} setup key
|
|
173
|
+
</button>
|
|
174
|
+
|
|
175
|
+
{showSecretKey && totpSecret && (
|
|
176
|
+
<div className={styles.secretKeyDisplay}>
|
|
177
|
+
<p className={styles.note}>
|
|
178
|
+
If you cannot scan the QR code, enter this key manually in your authenticator app:
|
|
179
|
+
</p>
|
|
180
|
+
<code className={styles.secretKey}>{totpSecret.secretKey}</code>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<p className={styles.note}>
|
|
185
|
+
After scanning, enter the 6-digit code from your authenticator app below.
|
|
186
|
+
</p>
|
|
187
|
+
<input
|
|
188
|
+
type="text"
|
|
189
|
+
inputMode="numeric"
|
|
190
|
+
value={verificationCode}
|
|
191
|
+
onChange={(e) => {
|
|
192
|
+
setVerificationCode(e.target.value.replace(/\D/g, ''));
|
|
193
|
+
if (errorMessage) setErrorMessage('');
|
|
194
|
+
}}
|
|
195
|
+
onKeyDown={(e) => {
|
|
196
|
+
if (e.key === 'Enter' && verificationCode.length === 6) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
void handleVerify();
|
|
199
|
+
}
|
|
200
|
+
}}
|
|
201
|
+
placeholder="123456"
|
|
202
|
+
maxLength={6}
|
|
203
|
+
className={styles.enrollmentInput}
|
|
204
|
+
disabled={isLoading}
|
|
205
|
+
autoComplete="one-time-code"
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
<div className={styles.buttonGroup}>
|
|
209
|
+
<button
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={handleVerify}
|
|
212
|
+
disabled={isLoading || verificationCode.length !== 6}
|
|
213
|
+
className={styles.primaryButton}
|
|
214
|
+
>
|
|
215
|
+
{isLoading ? 'Verifying…' : 'Complete Setup'}
|
|
216
|
+
</button>
|
|
217
|
+
|
|
218
|
+
{onBack && (
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={onBack}
|
|
222
|
+
disabled={isLoading}
|
|
223
|
+
className={styles.enrollmentSecondaryButton}
|
|
224
|
+
>
|
|
225
|
+
Back
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
};
|