-
Notifications
You must be signed in to change notification settings - Fork 0
feat(phase7): implement Phase 7 PRE-7 + 7.A — security foundation, break-glass, data incidents, provincial resources #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1590874
7f9a3d9
4cb0e48
a7a6081
e2ebfcb
b64fc15
dabc73b
9096fd9
127581b
067c61a
7a0052a
d7341ed
5ca1398
9ebac11
1a6b761
ba88787
64b6111
65d9612
230507d
a9c6bd9
ee7c432
88afba1
88d24fe
6815666
954df4c
0efada6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getAuth, multiFactor, TotpMultiFactorGenerator } from 'firebase/auth' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { TotpSecret } from 'firebase/auth' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function TotpEnrollmentPage() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const auth = getAuth() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [totpSecret, setTotpSecret] = useState<TotpSecret | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [verificationCode, setVerificationCode] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [enrolled, setEnrolled] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleGenerate() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = auth.currentUser | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError('You must be logged in to enroll TOTP.') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const session = await multiFactor(user).getSession() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const secret = await TotpMultiFactorGenerator.generateSecret(session) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTotpSecret(secret) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(err instanceof Error ? err.message : 'Failed to generate secret') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function handleEnroll() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = auth.currentUser | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user || !totpSecret) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const assertion = TotpMultiFactorGenerator.assertionForEnrollment( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| totpSecret, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| verificationCode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await multiFactor(user).enroll(assertion, 'Authenticator') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setEnrolled(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(err instanceof Error ? err.message : 'Enrollment failed') | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (enrolled) return <p>TOTP enrolled successfully.</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1>Enroll TOTP Authenticator</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {!totpSecret ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button onClick={() => void handleGenerate()}>Generate Secret</button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Secret key: <code>{totpSecret.secretKey}</code> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| QR URI:{' '} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <code> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {totpSecret.generateQrCodeUrl('Bantayog Alert', auth.currentUser?.email ?? 'admin')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </code> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="6-digit code" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={verificationCode} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| setVerificationCode(e.target.value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button onClick={() => void handleEnroll()}>Verify and Enroll</button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+62
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding input validation for verification code. The 6-digit TOTP code is passed directly to Firebase without client-side format validation. Adding a pattern or input type can improve UX: Proposed improvement <input
placeholder="6-digit code"
+ type="text"
+ inputMode="numeric"
+ pattern="[0-9]{6}"
+ maxLength={6}
value={verificationCode}
onChange={(e) => {
- setVerificationCode(e.target.value)
+ setVerificationCode(e.target.value.replace(/\D/g, '').slice(0, 6))
}}
/>
+<button
+ onClick={() => void handleEnroll()}
+ disabled={verificationCode.length !== 6}
+>
+ Verify and Enroll
+</button>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {error && <p role="alert">{error}</p>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -143,4 +143,72 @@ export const callables = { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'bulkAvailabilityOverride', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initiateBreakGlass: (payload: { codeA: string; codeB: string; reason: string }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload, { sessionId: string }>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'initiateBreakGlass', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deactivateBreakGlass: () => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<Record<string, never>>(functions, 'deactivateBreakGlass')({}).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| declareEmergency: (payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hazardType: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| affectedMunicipalityIds: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload, { alertId: string }>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'declareEmergency', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| declareDataIncident: (payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| incidentType: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| severity: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| affectedCollections: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| affectedDataClasses: string[] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| estimatedAffectedSubjects?: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| summary: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload, { incidentId: string }>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'declareDataIncident', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recordIncidentResponseEvent: (payload: { incidentId: string; phase: string; notes?: string }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload, { eventId: string }>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'recordIncidentResponseEvent', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setRetentionExempt: (payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| collection: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| documentId: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exempt: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => httpsCallable<typeof payload>(functions, 'setRetentionExempt')(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| approveErasureRequest: (payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| erasureRequestId: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| approved: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reason?: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload>(functions, 'approveErasureRequest')(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toggleMutualAidVisibility: (payload: { agencyId: string; visible: boolean }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'toggleMutualAidVisibility', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+179
to
+195
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add explicit Several wrappers omit the response type generic, causing ♻️ Proposed explicit void typing setRetentionExempt: (payload: {
collection: string
documentId: string
exempt: boolean
reason: string
- }) => httpsCallable<typeof payload>(functions, 'setRetentionExempt')(payload).then((r) => r.data),
+ }) => httpsCallable<typeof payload, void>(functions, 'setRetentionExempt')(payload).then(() => undefined),
approveErasureRequest: (payload: {
erasureRequestId: string
approved: boolean
reason?: string
}) =>
- httpsCallable<typeof payload>(functions, 'approveErasureRequest')(payload).then((r) => r.data),
+ httpsCallable<typeof payload, void>(functions, 'approveErasureRequest')(payload).then(() => undefined),
toggleMutualAidVisibility: (payload: { agencyId: string; visible: boolean }) =>
- httpsCallable<typeof payload>(
+ httpsCallable<typeof payload, void>(
functions,
'toggleMutualAidVisibility',
- )(payload).then((r) => r.data),
+ )(payload).then(() => undefined),This ensures callers get proper 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upsertProvincialResource: (payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id?: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quantity: number | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unit: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| location: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| available: boolean | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload, { id: string }>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'upsertProvincialResource', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| archiveProvincialResource: (payload: { id: string }) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| httpsCallable<typeof payload>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| functions, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'archiveProvincialResource', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )(payload).then((r) => r.data), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+209
to
+213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Same void-typing issue for ♻️ Proposed fix archiveProvincialResource: (payload: { id: string }) =>
- httpsCallable<typeof payload>(
+ httpsCallable<typeof payload, void>(
functions,
'archiveProvincialResource',
- )(payload).then((r) => r.data),
+ )(payload).then(() => undefined),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -96,6 +96,16 @@ | |||||
| - Capacitor void-return callbacks need braces: `return () => { clearInterval(id) }`. | ||||||
| - When refactoring from `refCount` to `Set<subscribers>`, remove ALL stale `refCount` references. | ||||||
|
|
||||||
| ## Phase 7 — Provincial Superadmin | ||||||
|
|
||||||
| - `@google-cloud/bigquery` `.table.query()` doesn't exist; use `bq.query()` directly for SQL queries. | ||||||
| - BigQuery query results are untyped; extract into typed helpers with `as unknown as RowType[]` to satisfy strict ESLint rules (`no-unsafe-member-access`, `no-unsafe-argument`). | ||||||
| - `@typescript-eslint/no-unnecessary-condition` flags `?.` on non-optional fields in function parameter types — use `.` when the type declares the field as required. | ||||||
| - Firestore path template literals (`db.doc(\`...\`)`) trigger `no-restricted-syntax`lint; use chained`.collection().doc()` instead. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the malformed code spans on this line.
📝 Proposed fix-- Firestore path template literals (`db.doc(\`...\`)`) trigger `no-restricted-syntax`lint; use chained`.collection().doc()` instead.
+- Firestore path template literals (`db.doc(\`...\`)`) trigger `no-restricted-syntax` lint; use chained `.collection().doc()` instead.📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 104-104: Spaces inside code span elements (MD038, no-space-in-code) 🤖 Prompt for AI Agents |
||||||
| - `@typescript-eslint/no-misused-promises` flags async onClick handlers; wrap with `() => void asyncFn()`. | ||||||
| - `bcryptjs` preferred over `bcrypt` in this repo — pure JS, no native compilation. | ||||||
| - `@google-cloud/logging` must be added as explicit dependency when using Cloud Logging API in triggers. | ||||||
|
|
||||||
| ## Misc | ||||||
|
|
||||||
| - `navigator.clipboard` in happy-dom often needs to be defined as an own property before spying. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export {}; | ||
| //# sourceMappingURL=mark-dispatch-unable-to-complete.test.d.ts.map |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
QR URI displayed as text instead of scannable QR code.
Users must manually copy the URI rather than scan a QR code. Consider rendering an actual QR code image using a library like
qrcode.react:Suggested improvement
🤖 Prompt for AI Agents