audit: fix all Critical and High security/reliability issues across monorepo
6-domain audit (API, web, mobile, lambdas, infra, QA) with fixes:
API security: scope internal API key middleware to allowed paths only,
return 401 on invalid key instead of falling through, remove unvalidated
JWT code path, sanitize error messages, add UpdateProposal validator,
remove status field from UpdateProposalRequest to prevent over-posting,
log swallowed exceptions in ProposalService.
Infrastructure: enforce SSL on all S3 buckets, encrypt SQS queues,
enable optional MFA on Cognito, add API Gateway access logging.
Lambdas: fix _retry_request undefined variable across all 4 Lambdas,
re-raise exceptions in pdf-extract/pdf-generate instead of swallowing,
add idempotency guard to suggestions Lambda.
Web: add ErrorBoundary, add auth loading state to ProtectedRoute,
add mutation error toasts in AdminWorkspace, fix dead Cognito link.
Mobile: add mutex to offline queue processing, distinguish permanent
vs retryable failures, register all screens for both roles, log sync
errors.
Swagger/OpenAPI: add Swashbuckle with JWT bearer security definition,
add ProducesResponseType attributes to key endpoints.
Includes AUDIT-REPORT.md with complete findings and CLAUDE.md project
instructions.
2026-05-27 15:33:27 -04:00
|
|
|
import React from 'react';
|
|
|
|
|
import { Box, Button, Typography } from '@mui/material';
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
hasError: boolean;
|
|
|
|
|
error: Error | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default class ErrorBoundary extends React.Component<Props, State> {
|
|
|
|
|
state: State = { hasError: false, error: null };
|
|
|
|
|
|
|
|
|
|
static getDerivedStateFromError(error: Error): State {
|
|
|
|
|
return { hasError: true, error };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
|
|
|
console.error('ErrorBoundary caught:', error, info.componentStack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
|
|
|
|
if (this.state.hasError) {
|
|
|
|
|
return (
|
ci(web): Phase 5 — Prettier check + Playwright smoke, org frontend workflow (#225)
* feat(web): adopt SHOC design system and shell layout (ADR 0003)
Port shoc-frontend-new dev's design system with its CSS-variable
single-token-source mechanism:
- src/styles/theme.css: SHOC token file ported verbatim (Montserrat/
DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/
shadow/sidebar/header token layers); fonts self-hosted via @fontsource
- src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme
adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows
tuple, component overrides; MUI v9 slot renames expressed as class
selectors); theme.ts is now a re-export
- Shell: SHOC composition (sidebar column + sticky gradient topbar +
scrolling main); sidebar 244px/76px collapse with brand header row,
grouped nav, SHOC active treatment (white card + 3px accent bar);
topbar 100-degree gradient, surface hamburger, gradient avatar pill
- Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as
sx; wordmark subtitle localized to PROPOSAL SYSTEM)
- Login: SHOC auth-card treatment (centered 384px card on #f9fafb)
- Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed;
remaining hardcoded hexes replaced with tokens; lucide-react for
shell/nav icons per SHOC convention
Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots
pixel-sampled against the extracted SHOC spec (all hard values exact,
no blocking deviations).
* feat(contracts): adopt shared api-contracts in web, add zod schemas and ProblemDetails codes
Closes WEB-M5 (web hand-duplicated wire types, standing drift risk):
- shared/api-contracts: rewritten as the authoritative superset of the
.NET DTOs (ProposalListItem/ProposalDetail with poNumber and
submittedByName, line item requests, customers, pricing library,
dashboard, audit, sites, auth, presigned upload, ApiProblem); stale
Proposal/UpdateLineItemsRequest shapes removed
- shared/api-contracts/src/schemas.ts: zod runtime schemas coupled to
every wire type via `satisfies z.ZodType<T>` (schema/type drift is now
a compile error); separate entrypoint so type-only consumers (mobile)
never pull zod
- web: imports @proposal-system/api-contracts (file: dep + tsconfig
paths + vite preserveSymlinks); all 7 lib/api modules re-export shared
types so page imports stay stable; enum unions tightened
(PricingLibraryPage form state now ServiceCategory-typed)
- fix(web): customer create/update sent a singular `address` field the
API silently dropped (contract is addresses: string[], CustomerDtos.cs)
- addresses now round-trip, extra addresses preserved on edit
- api: ProblemDetails responses carry a machine-readable top-level
`code` (SHOC error-code vocabulary): ValidationFailed,
InvalidStateTransition, NotFound, Unauthorized, InternalError; new
BusinessRuleException(code, message) maps to 422 with its code;
GlobalExceptionHandlerTests cover the full mapping (wire contract)
Cross-checked .NET DTOs vs TS types vs zod schemas with the
orchestrator scanner (Gemini): core domains consistent; internal-only
DTOs (FileDtos vendor/lambda surface, SimilarProposalDtos, UserDtos
admin surface) intentionally uncovered.
Verify: dotnet 166/166, web tsc + vitest 26/26 + build, mobile tsc,
shared tsc all green.
* feat(web): domain-layer conventions doc + react-hook-form deps (Phase 4 prep)
* feat(web): scaffold domain module layer (proposals, lineItems, customers, pricingLibrary, admin, sites)
Additive-only: pages still use lib/api/* and constants/queryKeys.ts until
the page-migration agents run. Each domain ships api.ts (HTTP moved from
lib/api), types.ts (contract re-exports + view types), schemas.ts (contract
schema re-exports + form schemas with toRequest mappers), and use-cases.ts
(TanStack Query v5 hooks + hierarchical query keys, mirroring current page
invalidations and toast-on-error behavior).
Adds an explicit vite/vitest alias for the
@proposal-system/api-contracts/schemas subpath (package has no exports map)
plus a schema/mapper smoke test suite.
* refactor(web): proposal pages on domain layer, proposal form on react-hook-form
* refactor(web): admin workspace on domain layer
* refactor(web): customer management on domain layer + react-hook-form
* refactor(web): pricing library on domain layer + react-hook-form
* refactor(web): dashboards on domain layer
* test(web): domain use-case hook coverage
* refactor(web): finish domain-layer integration — migrate straggler components, delete legacy api modules
- SimilarProposalsPanel -> useSimilarProposals (domain/admin); inline
SimilarProposal type replaced by domain/admin/types (identical shape);
query key joins the admin hierarchical key space
- LineItemEditor type imports -> domain/lineItems/types
- Delete now-orphaned lib/api/{proposals,lineItems,customers,
pricingLibrary,admin,sites}.ts, constants/queryKeys.ts,
hooks/usePaginatedList.ts (lib/api/client.ts + auth.ts stay per
domain README rule 5)
Verify: tsc clean, vitest 52/52, build OK, Playwright smoke of the
authed shell renders on domain hooks.
* fix(web): apply Phase 4 code-review findings (10 correctness + 4 cleanup)
Correctness:
- State-transition mutations now invalidate every cached view via
invalidateProposalViews (detail + line items + lists + stats + admin
dashboard) — approving no longer leaves a stale queue for the
5-minute staleTime
- Presigned S3 PUT moved to proposals/api.ts with res.ok check — a
rejected upload is no longer confirmed as uploaded
- toCustomerRequest always sends contactEmail ('' clears); API create
path normalizes empty->null to match the update path — customer
emails can now be cleared from the UI
- Shared Number-based numeric form fields (domain/shared/formFields):
'12abc' no longer silently coerces to 12 in the pricing library
- Customer create/update invalidate customersKeys.all so cached search
autocompletes see new customers
- AdminWorkspace clears dirty right after a successful implicit save,
before approve — no false unsaved-changes prompt when approve fails
- ProposalFormPage submit gate and missing-fields caption derive from
ONE checks list (missing customer is now listed)
- Empty states gated on !err in ProposalListPage/AdminDashboard — no
contradictory error + 'no proposals' UI
- VendorDataPanel migrated to useVendorProposals (kills the divergent
['vendorProposals', id] cache key and the inline apiClient query)
- useCustomerList/usePricingLibraryList get keepPreviousData — no
TablePagination out-of-range flash on page change
Cleanup:
- Dead speculative hooks removed (useCreate/BulkUpdate/DeleteLineItem,
useUpdateProposal, useProposalHistory/Audit, lineItemRowFormSchema,
toUpdateLineItemEntry); tests moved to the live save path
(useSaveProposalWorkspace)
- Shared useDebouncedValue hook replaces 4 drifted inline debounce
copies (one leaked its timer on unmount, two hardcoded 300ms);
DEBOUNCE_AUTOCOMPLETE=300 named
- Fix: WEB-H5 / WEB-H6 finding-ID markers restored at the relocated
onError handlers (CLAUDE.md traceability)
- shared/api-contracts gains an exports map; /schemas resolver alias
deduplicated from 3 copies to the tsconfig paths mapping
Verify: tsc clean, vitest 51/51 (tests updated to pin the new
invalidation/mapper behavior + new '12abc' rejection test),
vite build OK, dotnet 166/166.
* refactor(web): fold Redux auth/ui slices into SHOC-shape auth context + storage module
Phase 4 tail of the SHOC-alignment plan. Matches SHOC's auth shape
(lib/auth storage module + providers/ context split) while keeping the
deliberate divergences:
- sessionStorage, not localStorage (WEB-C1 stands; SHOC's localStorage
is on the SHOULD-NOT-ALIGN list)
- token acquisition stays in the auth pages (Cognito code exchange /
dev-login) — the provider only owns session state
- 401 interceptor clears storage directly (WEB-M2 behavior preserved;
full-page redirect resets provider state)
Sidebar open state moves to plain layout state in App passed down as
props (SHOC (protected)/_layout.tsx pattern), keeping localStorage
persistence. Drops @reduxjs/toolkit and react-redux.
Tests: authSlice tests replaced by authStorage + AuthProvider suites
(QA-C5 coverage preserved); client interceptor tests updated for the
storage-based 401 path. 59 vitest green, tsc clean, vite build OK.
Verified end-to-end headless: login redirect, seeded-session shell,
sidebar toggle persistence, logout, expired/malformed token handling,
RoleGuard bounce; recipe persisted as web/.claude/skills/verify.
* fix(web): harden auth session teardown per /sh-security-review findings
- AUTH-L1 (confirmed medium): logout() now clears the react-query cache —
the singleton cache survived SPA logout, serving the previous
principal's cached GETs to the next login in the same tab for up to
staleTime with no server round-trip.
- AUTH-L3 (confirmed low): isTokenValid decodes base64url before atob —
valid Cognito JWTs containing '-'/'_' in the payload segment were
misclassified as expired (login lockout/loop; inherited from the old
authSlice).
- AUTH-L2 (unverified, hardened anyway): 401 interceptor broadcasts
AUTH_SESSION_CLEARED_EVENT so AuthProvider drops in-memory state
synchronously, restoring the old Redux atomic-clear semantics.
- INJ-1 (unverified, hardened anyway): Authorization header only set
when the stored token is a string.
Each fix pinned by a test; 63 vitest green, tsc clean.
* docs: web stack row reflects auth-context refactor (Redux removed, MUI v9)
* ci(web): Phase 5 — prettier check + Playwright smoke via org frontend workflow
Converts the web CI job from ci-typescript-cdk.yaml (typecheck only) to
ci-typescript-frontend.yaml: format:check, build (tsc -b included),
vitest, and a Playwright chromium smoke. Folds the standalone Web Tests
job into it (aggregator needs updated). Pure CI — no AWS secrets.
The smoke (e2e/smoke.spec.ts) drives dev-login → dashboard shell →
proposal list, plus the unauthenticated bounce, against a fully mocked
API (pathname-anchored route interception — a '**/api/**' glob would
swallow vite's /src/lib/api/* module URLs). Config mirrors SHOC's
playwright.config.ts (port 4173, chromium, dev-server webServer).
Prettier: singleQuote + printWidth 100 to match the existing codebase
style; lint intentionally not added (no ESLint config yet — run-lint
false, out of Phase 5 scope). rollback = revert this workflow file.
* style(web): prettier format pass (mechanical)
npx prettier --write . with the new .prettierrc (singleQuote,
printWidth 100). No functional changes — enforced by format:check in
CI from this PR on.
2026-07-13 21:02:20 -04:00
|
|
|
<Box
|
|
|
|
|
sx={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
minHeight: '100vh',
|
|
|
|
|
gap: 2,
|
|
|
|
|
p: 4,
|
|
|
|
|
}}
|
|
|
|
|
>
|
audit: fix all Critical and High security/reliability issues across monorepo
6-domain audit (API, web, mobile, lambdas, infra, QA) with fixes:
API security: scope internal API key middleware to allowed paths only,
return 401 on invalid key instead of falling through, remove unvalidated
JWT code path, sanitize error messages, add UpdateProposal validator,
remove status field from UpdateProposalRequest to prevent over-posting,
log swallowed exceptions in ProposalService.
Infrastructure: enforce SSL on all S3 buckets, encrypt SQS queues,
enable optional MFA on Cognito, add API Gateway access logging.
Lambdas: fix _retry_request undefined variable across all 4 Lambdas,
re-raise exceptions in pdf-extract/pdf-generate instead of swallowing,
add idempotency guard to suggestions Lambda.
Web: add ErrorBoundary, add auth loading state to ProtectedRoute,
add mutation error toasts in AdminWorkspace, fix dead Cognito link.
Mobile: add mutex to offline queue processing, distinguish permanent
vs retryable failures, register all screens for both roles, log sync
errors.
Swagger/OpenAPI: add Swashbuckle with JWT bearer security definition,
add ProducesResponseType attributes to key endpoints.
Includes AUDIT-REPORT.md with complete findings and CLAUDE.md project
instructions.
2026-05-27 15:33:27 -04:00
|
|
|
<Typography variant="h5">Something went wrong</Typography>
|
|
|
|
|
<Typography color="text.secondary" sx={{ maxWidth: 480, textAlign: 'center' }}>
|
|
|
|
|
An unexpected error occurred. Please try refreshing the page.
|
|
|
|
|
</Typography>
|
|
|
|
|
<Button variant="contained" onClick={() => window.location.assign('/')}>
|
|
|
|
|
Return to Dashboard
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.props.children;
|
|
|
|
|
}
|
|
|
|
|
}
|