@striae-org/striae 5.4.0 → 5.4.1

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.
@@ -316,6 +316,13 @@ export const MFAVerification = ({ resolver, onSuccess, onError, onCancel }: MFAV
316
316
  <p className={styles.description}>
317
317
  Enter the 6-digit code from your authenticator app.
318
318
  </p>
319
+ <p className={styles.note}>
320
+ Lost access to your authenticator app?{' '}
321
+ <a href="https://striae.org/support" target="_blank" rel="noopener noreferrer">
322
+ Contact support
323
+ </a>{' '}
324
+ to recover your account.
325
+ </p>
319
326
  <input
320
327
  type="text"
321
328
  inputMode="numeric"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@striae-org/striae",
3
- "version": "5.4.0",
3
+ "version": "5.4.1",
4
4
  "private": false,
5
5
  "description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
6
6
  "license": "Apache-2.0",
@@ -40,23 +40,22 @@
40
40
  "scripts/",
41
41
  "functions/",
42
42
  "public/",
43
- "workers/*/package.json",
44
- "workers/*/src/**/*.example.ts",
45
- "workers/*/src/**/*.ts",
46
- "workers/pdf-worker/scripts/*.js",
43
+ "workers/",
44
+ "!workers/*/.wrangler",
45
+ "!workers/*/package-lock.json",
46
+ "!workers/*/worker-configuration.d.ts",
47
+ "!workers/*/wrangler.jsonc",
47
48
  "!workers/*/src/**/*worker.ts",
48
49
  "!workers/pdf-worker/src/assets/**/*",
49
50
  "workers/pdf-worker/src/assets/generated-assets.example.ts",
50
51
  "!workers/pdf-worker/src/formats/**/*",
51
52
  "workers/pdf-worker/src/formats/format-striae.ts",
52
- "workers/pdf-worker/src/report-types.ts",
53
- "workers/*/wrangler.jsonc.example",
54
53
  ".env.example",
55
54
  "primershear.emails.example",
56
55
  "firebase.json",
57
56
  "tsconfig.json",
58
57
  "vite.config.ts",
59
- "worker-configuration.d.ts",
58
+ "/worker-configuration.d.ts",
60
59
  "wrangler.toml.example",
61
60
  "LICENSE"
62
61
  ],
@@ -83,6 +82,7 @@
83
82
  "preview": "npm run build && wrangler pages dev",
84
83
  "cf-typegen": "wrangler types",
85
84
  "enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
85
+ "unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
86
86
  "update-versions": "node ./scripts/update-markdown-versions.cjs",
87
87
  "update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
88
88
  "deploy-config": "bash ./scripts/deploy-config.sh",
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Admin script to unenroll a user's TOTP MFA factor via Firebase Admin SDK.
3
+ * Run with: npm run unenroll-totp-mfa -- <uid>
4
+ *
5
+ * Requires app/config/admin-service.json (gitignored service account key).
6
+ * Docs: https://firebase.google.com/docs/auth/admin/manage-users#unenroll_a_user_from_mfa
7
+ */
8
+
9
+ import { createRequire } from 'module';
10
+ import { fileURLToPath } from 'url';
11
+ import { dirname, resolve } from 'path';
12
+ import { initializeApp, cert, getApps } from 'firebase-admin/app';
13
+ import { getAuth } from 'firebase-admin/auth';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+
18
+ const uid = process.argv[2];
19
+
20
+ if (!uid) {
21
+ console.error('\n❌ No UID provided.');
22
+ console.error('\nUsage: npm run unenroll-totp-mfa -- <uid>');
23
+ process.exit(1);
24
+ }
25
+
26
+ const serviceAccountPath = resolve(__dirname, '../app/config/admin-service.json');
27
+
28
+ let serviceAccount;
29
+ try {
30
+ serviceAccount = require(serviceAccountPath);
31
+ } catch {
32
+ console.error(`\n❌ Could not load service account key from:\n ${serviceAccountPath}`);
33
+ console.error('\nMake sure app/config/admin-service.json exists (it is gitignored).');
34
+ process.exit(1);
35
+ }
36
+
37
+ if (getApps().length === 0) {
38
+ initializeApp({ credential: cert(serviceAccount) });
39
+ }
40
+
41
+ const auth = getAuth();
42
+
43
+ console.log(`\n🔍 Fetching MFA factors for UID: ${uid}...`);
44
+
45
+ let userRecord;
46
+ try {
47
+ userRecord = await auth.getUser(uid);
48
+ } catch (err) {
49
+ console.error(`\n❌ Could not fetch user record for UID: ${uid}`);
50
+ console.error(err?.message ?? err);
51
+ process.exit(1);
52
+ }
53
+
54
+ const enrolledFactors = userRecord.multiFactor?.enrolledFactors ?? [];
55
+ const totpFactors = enrolledFactors.filter((f) => f.factorId === 'totp');
56
+ const remainingFactors = enrolledFactors.filter((f) => f.factorId !== 'totp');
57
+
58
+ if (totpFactors.length === 0) {
59
+ console.log(`\nℹ️ No TOTP MFA factors found for UID: ${uid}`);
60
+ console.log(' Nothing to unenroll.');
61
+ process.exit(0);
62
+ }
63
+
64
+ console.log(`\n Found ${totpFactors.length} TOTP factor(s):`);
65
+ for (const factor of totpFactors) {
66
+ console.log(` - ${factor.uid} (displayName: ${factor.displayName ?? 'n/a'}, enrolled: ${factor.enrollmentTime})`);
67
+ }
68
+
69
+ try {
70
+ await auth.updateUser(uid, {
71
+ multiFactor: {
72
+ enrolledFactors: remainingFactors,
73
+ },
74
+ });
75
+
76
+ console.log(`\n✅ Successfully unenrolled ${totpFactors.length} TOTP factor(s) for UID: ${uid}`);
77
+ console.log(' The user will need to re-enroll TOTP on their next login.');
78
+ } catch (err) {
79
+ console.error(`\n❌ Failed to unenroll TOTP factor(s) for UID: ${uid}`);
80
+ console.error(err?.message ?? err);
81
+ process.exit(1);
82
+ }
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }
@@ -0,0 +1,12 @@
1
+ # http://editorconfig.org
2
+ root = true
3
+
4
+ [*]
5
+ indent_style = tab
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
10
+
11
+ [*.yml]
12
+ indent_style = space
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "semi": true,
5
+ "useTabs": true
6
+ }