-
Notifications
You must be signed in to change notification settings - Fork 0
fix: code quality and security refactor β audit-driven fixes across monorepo #61
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
Merged
Merged
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
118a6cc
fix(sms-parser): fix 2 failing tests by using barangays present in faβ¦
claude 3e568a0
fix(validators): add include pattern to prevent duplicate test execution
claude c3785ee
fix(citizen-pwa): type bare catch blocks for private-mode storage faiβ¦
claude 5964a94
fix(packages): type bare catch blocks in sms-parser and validators
claude 661f5e8
fix(functions): improve error handling and visibility in critical paths
claude 41ac6d8
test(functions): add critical auth boundary tests for https-error hanβ¦
claude b3ac175
docs: add code quality audit findings and progress tracking
claude e4217e9
chore(validators): update compiled lib artifacts for msisdn and idempβ¦
claude cddbb24
fix: address PR review comments β Web Crypto guard and FCM error logging
claude 57a7f4f
fix: address CodeRabbit review comments β enum coverage, PII logging,β¦
claude 3da508a
fix(idempotency): strengthen Web Crypto guard to handle null subtle
claude 21564cc
fix(shared-validators): circular reference detection, salt validationβ¦
claude 25cd8db
fix(functions): validate SMS_MSISDN_ENCRYPTION_KEY format before decrβ¦
claude 271b796
fix(citizen-pwa): discriminate storage errors and reduce network probβ¦
claude b0ecaa2
fix(shared-sms-parser): narrow MODULE_NOT_FOUND fallback to @bantayogβ¦
claude 20f33c3
docs(audit): refresh refactor-audit with current bare-catch counts anβ¦
claude fe13c0d
fix(citizen-pwa): log unexpected storage errors in Step2WhoWhere
claude c67545c
fix(citizen-pwa): move timeout cleanup to finally block in useOnlineSβ¦
claude 8a2b5e1
fix(shared-validators): preserve original error in idempotency catch
claude 1ba7782
fix(shared-validators): check typeof before accessing salt.length
claude a0ad82c
fix(citizen-pwa,docs): address CodeRabbit review comments for PR #61
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| # Refactor Audit β 2026-04-23 | ||
|
|
||
| **Scope:** Full monorepo (apps/_, packages/_, functions) | ||
| **Health Check:** Lint/Typecheck β 25/25 passing | Tests β All passing (shared-sms-parser fixed) | ||
|
|
||
| --- | ||
|
|
||
| ## π΄ P0 β Fix Before Anything Else | ||
|
|
||
| ### ~~1. `shared-sms-parser` has 2 failing tests~~ β RESOLVED | ||
|
|
||
| - **Status:** Fixed in this PR β tests now pass (13/13) | ||
| - **Fix:** Updated test expectations to use barangays present in fallback gazetteer (LANG for ambiguous match, ANAHAW for exact match) | ||
|
|
||
| --- | ||
|
|
||
| ## π P1 β High Risk / Hard to Maintain | ||
|
|
||
| ### 2. `Step2WhoWhere.tsx` is a 707-line god component | ||
|
|
||
| - **File:** `apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx` | ||
| - **Issues:** | ||
| - 707 lines in a single React component | ||
| - ~~2 bare `catch {}` blocks~~ β Fixed β now typed as `catch (_err: unknown)` for private-mode storage failures | ||
| - Mixes GPS logic, municipality selection, form validation, UI rendering | ||
| - **Impact:** Impossible to test. Every change risks regressing the entire citizen reporting flow. | ||
| - **Action:** Extract sub-components: `GpsButton`, `MunicipalitySelector`, `BarangaySelector`, `ContactFields`. Extract hooks: `useGpsLocation`, `useMunicipalityBarangays`. | ||
| - **Effort:** Medium (~3-5 files, ~150 lines) | ||
|
|
||
| ### 3. `inbound.ts` is a 541-line parser with multiple responsibilities | ||
|
|
||
| - **File:** `packages/shared-sms-parser/src/inbound.ts` | ||
| - **Issues:** | ||
| - Contains gazetteer loading, Levenshtein distance, auto-reply building, AND parsing logic | ||
| - 6 `@ts-expect-error` comments (lines 360-376) indicating fragile array logic | ||
| - ~~1 bare `catch {}` (line 45)~~ β Fixed β now uses `catch (err: unknown)` with selective fallback/rethrow (only swallows `MODULE_NOT_FOUND` for `@bantayog/shared-data`) | ||
| - **Impact:** High cyclomatic complexity makes bugs likely. | ||
| - **Action:** Split into `levenshtein.ts`, `gazetteer.ts`, `auto-reply.ts`, `parser.ts`. | ||
| - **Effort:** Medium (~4 files, ~80 lines of changes) | ||
|
|
||
| ### 4. `dispatch-responder.ts` is 307 lines of callable logic | ||
|
|
||
| - **File:** `functions/src/callables/dispatch-responder.ts` | ||
| - **Impact:** Core responder dispatch function. Long functions = hard to reason about security rules. | ||
| - **Action:** Extract validation, notification, and Firestore write logic into separate functions. | ||
| - **Effort:** Medium (~1 file refactored, ~2 new files) | ||
|
|
||
| --- | ||
|
|
||
| ## π‘ P2 β Structural / Consistency Debt | ||
|
|
||
| ### 5. Entire apps/packages have zero tests | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| | Package/App | Source Files | Tests | | ||
| | ----------------------- | ------------ | ----- | | ||
| | `apps/admin-desktop` | 18 | **0** | | ||
| | `apps/responder-app` | 23 | **1** | | ||
| | `packages/shared-data` | 1 | **0** | | ||
| | `packages/shared-types` | 8 | **0** | | ||
| | `packages/shared-ui` | 2 | **0** | | ||
|
|
||
| - **Impact:** Regressions in admin/responder flows are only caught in manual QA or production. | ||
| - **Action:** Add characterization tests for critical paths first: | ||
| 1. `admin-desktop`: `LoginPage`, `TriageQueuePage`, `DispatchModal` | ||
| 2. `responder-app`: `DispatchListPage`, `DispatchDetailPage`, `useAcceptDispatch` | ||
| 3. `shared-ui`: render tests for shared components | ||
| - **Effort:** Large (spread across sprints) | ||
|
|
||
| ### 6. Auth boilerplate duplicated across 2 apps | ||
|
|
||
| - **Files:** | ||
| - `apps/admin-desktop/src/app/auth-provider.tsx` vs `apps/responder-app/src/app/auth-provider.tsx` | ||
| - `apps/admin-desktop/src/app/protected-route.tsx` vs `apps/responder-app/src/app/protected-route.tsx` | ||
| - `apps/admin-desktop/src/app/firebase.ts` vs `apps/responder-app/src/app/firebase.ts` | ||
| - **Issues:** Same patterns, slightly different implementations (claims types, role checks). Fixes in one don't propagate. | ||
| - **Action:** Move auth provider + protected route to `shared-ui` or `shared-firebase`. Keep role-checking as props/config. | ||
| - **Effort:** Medium (~2 new files in shared package, ~4 files deleted from apps) | ||
|
|
||
| ### 7. Inconsistent error handling (5 catch patterns) | ||
|
|
||
| - **Patterns found:** | ||
| - `catch (err: unknown)` β ~51 occurrences | ||
| - `catch (err)` β 13 occurrences (implicit any) | ||
| - `catch (error)` β 5 occurrences | ||
| - `catch (e: unknown)` β 2 occurrences | ||
| - `catch {}` β **0** in production source (2 in `scripts/`, not production) | ||
| - **Files with bare `catch {}` (remaining):** | ||
| - `scripts/phase-3c/acceptance.ts` (2Γ) β scripts, not production source | ||
| - `functions/src/services/fcm-send.ts` (1Γ) β outer catch intentional for retry logic (has server-side console.error) | ||
| - `functions/src/triggers/inbox-reconciliation-sweep.ts` (1Γ) β transaction contention intentional skip (has comment) | ||
| - **Recently fixed:** | ||
| - ~~`apps/citizen-pwa/src/components/SubmitReportForm/Step2WhoWhere.tsx`~~ β | ||
| - ~~`apps/citizen-pwa/src/services/draft-store.ts`~~ β | ||
| - ~~`apps/citizen-pwa/src/hooks/useOnlineStatus.ts`~~ β | ||
| - ~~`packages/shared-sms-parser/src/inbound.ts`~~ β | ||
| - ~~`packages/shared-validators/src/msisdn.ts`~~ β | ||
| - ~~`functions/src/services/sms-providers/semaphore.ts`~~ β | ||
| - ~~`functions/src/triggers/on-media-finalize.ts`~~ β | ||
| - ~~`functions/src/http/sms-inbound.ts`~~ β | ||
| - ~~`functions/src/firestore/sms-inbound-processor.ts`~~ β | ||
| - **Impact:** Silent failures in production. Error monitoring tools see nothing. | ||
| - **Action:** Enforce `catch (err: unknown) { logError(err) }` via lint rule or codemod. Convert remaining `catch (err)` and `catch (error)` implicit-any patterns. | ||
| - **Effort:** Medium (~14 files, ~30 lines) | ||
|
|
||
| ### 8. `console.log` in production code | ||
|
|
||
| - **File:** `packages/shared-validators/src/logging.ts:87` | ||
| - **Impact:** Pollutes production logs. Could leak PII. | ||
| - **Action:** Replace with structured logger or remove. | ||
| - **Effort:** Tiny (~1 file, ~3 lines) | ||
|
|
||
| --- | ||
|
|
||
| ## π’ P3 β Cleanup / Polish | ||
|
|
||
| ### 9. `any` types in source and tests | ||
|
|
||
| - **Source files:** | ||
| - `functions/src/services/fcm-send.ts` β `batchResponse: any` | ||
| - **Test files:** | ||
| - `functions/src/__tests__/rules/*.test.ts` β `db: any` | ||
| - `functions/src/__tests__/callables/close-report.test.ts` β `doc: any` | ||
| - `functions/src/__tests__/acceptance/phase-4a-acceptance.test.ts` β `db: any`, `rtdb: any` | ||
| - **Note:** Test file `any` casts under `functions/src/__tests__/` are intentional for Firebase emulator compatibility and should NOT be flagged as tech-debt. These are required for emulator mock setup. | ||
| - **Action:** Replace source file `any` usages with proper Firestore types or `unknown`. Test file casts are exempt. | ||
| - **Effort:** Small | ||
|
|
||
| ### 10. Single lingering TODO | ||
|
|
||
| - **File:** `packages/shared-validators/src/sms-templates.ts:1` | ||
| - **Text:** `// TODO(phase-5): move template bodies to Firestore for CMS-driven editing.` | ||
| - **Action:** Ticket it or implement if Phase 5 is active. | ||
| - **Effort:** N/A (planning) | ||
|
|
||
| --- | ||
|
|
||
| ## Recommended Execution Order | ||
|
|
||
| 1. **P1:** Extract `Step2WhoWhere.tsx` into sub-components (2-3 sessions) | ||
| 2. **P1:** Split `inbound.ts` into modules (gazetteer, Levenshtein, auto-reply, parser) (1-2 sessions) | ||
| 3. **P2:** Add tests to `admin-desktop` critical paths (3-4 sessions) | ||
| 4. **P2:** Consolidate auth provider into `shared-firebase` (2 sessions) | ||
| 5. **P2:** Standardize remaining `catch (err)` / `catch (error)` implicit-any patterns (1 session) | ||
| 6. **P3:** Remove `any` types and `console.log` (1 session) | ||
|
|
||
| --- | ||
|
|
||
| ## Stats | ||
|
|
||
| - **Total source files:** ~250 | ||
| - **Total test files:** ~410 (but heavily concentrated in `functions`) | ||
| - **Lines of code:** ~14,665 | ||
| - **Test coverage gaps:** 5 packages/apps at 0-1 tests | ||
| - **Bare `catch {}` blocks:** 0 in production source (2 in scripts) | ||
| - `console.log` in src: 1 | ||
| - `TODO` in src: 1 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { HttpsError } from 'firebase-functions/v2/https' | ||
| import { | ||
| BANTAYOG_TO_HTTPS_CODE, | ||
| bantayogErrorToHttps, | ||
| requireAuth, | ||
| } from '../../callables/https-error.js' | ||
| import { BantayogError, BantayogErrorCode } from '@bantayog/shared-validators' | ||
|
|
||
| describe('BANTAYOG_TO_HTTPS_CODE', () => { | ||
| it('maps every BantayogErrorCode to a FunctionsErrorCode', () => { | ||
| // Iterate actual enum values, not map keys, to catch unmapped entries | ||
| const codes = Object.values(BantayogErrorCode).filter( | ||
| (value): value is BantayogErrorCode => typeof value === 'string', | ||
| ) | ||
| expect(codes.length).toBeGreaterThan(0) | ||
| for (const code of codes) { | ||
| expect(BANTAYOG_TO_HTTPS_CODE[code]).toBeDefined() | ||
| expect(typeof BANTAYOG_TO_HTTPS_CODE[code]).toBe('string') | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }) | ||
| }) | ||
|
|
||
| describe('bantayogErrorToHttps', () => { | ||
| it('converts a BantayogError to an HttpsError with the right code', () => { | ||
| const err = new BantayogError(BantayogErrorCode.VALIDATION_ERROR, 'bad input', { field: 'x' }) | ||
| const httpsErr = bantayogErrorToHttps(err) | ||
| expect(httpsErr).toBeInstanceOf(HttpsError) | ||
| expect(httpsErr.code).toBe('invalid-argument') | ||
| expect(httpsErr.message).toBe('bad input') | ||
| expect(httpsErr.details).toEqual({ field: 'x' }) | ||
| }) | ||
|
|
||
| it('converts NOT_FOUND to not-found', () => { | ||
| const err = new BantayogError(BantayogErrorCode.NOT_FOUND, 'missing') | ||
| const httpsErr = bantayogErrorToHttps(err) | ||
| expect(httpsErr.code).toBe('not-found') | ||
| }) | ||
| }) | ||
|
|
||
| describe('requireAuth', () => { | ||
| it('throws unauthenticated when request.auth is null', () => { | ||
| expect(() => requireAuth({ auth: null }, ['municipal_admin'])).toThrow(HttpsError) | ||
| expect(() => requireAuth({ auth: null }, ['municipal_admin'])).toThrow('sign-in required') | ||
| }) | ||
|
|
||
| it('throws unauthenticated when request.auth is undefined', () => { | ||
| expect(() => requireAuth({}, ['municipal_admin'])).toThrow(HttpsError) | ||
| }) | ||
|
|
||
| it('throws permission-denied when role is not in allowed list', () => { | ||
| const request = { | ||
| auth: { | ||
| uid: 'u1', | ||
| token: { role: 'citizen' }, | ||
| }, | ||
| } | ||
| expect(() => requireAuth(request, ['municipal_admin'])).toThrow('role citizen is not allowed') | ||
| }) | ||
|
|
||
| it('throws permission-denied when role is missing', () => { | ||
| const request = { | ||
| auth: { | ||
| uid: 'u1', | ||
| token: {}, | ||
| }, | ||
| } | ||
| expect(() => requireAuth(request, ['municipal_admin'])).toThrow('role undefined is not allowed') | ||
| }) | ||
|
|
||
| it('returns uid and claims when role is allowed', () => { | ||
| const request = { | ||
| auth: { | ||
| uid: 'u1', | ||
| token: { role: 'municipal_admin', municipalityId: 'm1' }, | ||
| }, | ||
| } | ||
| const result = requireAuth(request, ['municipal_admin', 'superadmin']) | ||
| expect(result.uid).toBe('u1') | ||
| expect(result.claims).toEqual({ | ||
| role: 'municipal_admin', | ||
| municipalityId: 'm1', | ||
| }) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.