# Proposal System ![.NET](https://img.shields.io/badge/.NET-512BD4?logo=dotnet&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white) ![AWS CDK](https://img.shields.io/badge/AWS-CDK-FF9900?logo=amazonaws&logoColor=white) ![CI](https://github.com/Sea-Haven-Industries/proposal-system/actions/workflows/ci.yaml/badge.svg) Internal proposal management platform for Sea Haven Industries. Dispatchers submit proposal requests, AI generates draft line items from historical data via Bedrock RAG, admins review and approve in a pricing workspace, and the system produces branded PDFs for delivery. ## Architecture Overview Monorepo with five primary services: - **.NET 8 API** -- Clean Architecture REST API hosted on Lambda behind API Gateway (JWT-authorized) with Function URL (AWS_IAM) for internal access - **React 19 Web** -- MUI v9 admin/dispatcher workspace served via CloudFront + S3 - **React Native Mobile** -- iOS-first field app for dispatchers (offline-capable) - **Python Lambdas** -- PDF extraction, PDF generation, library ingestion, AI suggestions, Aurora pgvector bootstrap - **CDK Infrastructure** -- Three TypeScript stacks managing all AWS resources ## Repository Structure ``` proposal-system/ ├── api/ .NET 8 Web API (Lambda-hosted, EF Core + PostgreSQL) ├── web/ React 19 + MUI v9 + Vite frontend ├── mobile/ React Native 0.86 iOS app ├── lambdas/ Python 3.12 processing functions (arm64) ├── infra/ CDK TypeScript (3 stacks) ├── shared/ TypeScript API contracts (shared between web + mobile) ├── scripts/ Post-deploy and utility scripts ├── .github/ CI/CD workflows └── docker-compose.yml ``` ## Tech Stack | Component | Technologies | |---|---| | API | .NET 8, ASP.NET Core, EF Core + Npgsql, FluentValidation, Cognito JWT, Amazon.Lambda.AspNetCoreServer | | Web | React 19, TypeScript, MUI v9, Vite, TanStack Query, react-hook-form + zod, axios | | Mobile | React Native CLI 0.86, React 19, React Native Paper, React Navigation, react-native-app-auth (PKCE), amazon-cognito-identity-js (SRP), Keychain, offline draft queue | | Lambdas | Python 3.12, arm64, pdfplumber, reportlab, httpx, boto3 | | Infrastructure | CDK TypeScript (aws-cdk-lib pinned exact, kept current by Dependabot) | | AI/RAG | Bedrock Knowledge Base (Titan Embeddings v2), Aurora PostgreSQL + pgvector vector store (see ADR 0001), Claude Sonnet via Bedrock cross-region inference | | Auth | Cognito User Pool + Google OAuth IdP (groups: dispatchers, admins, sysadmins) | ## AWS Resources All resources are in **us-east-1** (account 328440206208). | CDK Stack | Key Resources | |---|---| | `proposal-system-foundation` | Aurora PostgreSQL 15 Serverless v2 (RDS Data API, pgvector), S3 buckets, SQS queue + DLQ, Cognito user pool, Secrets Manager | | `proposal-system-compute` | API Gateway HTTP API (JWT authorizer + access logging), .NET 8 API Lambda + Function URL (AWS_IAM), Python Lambdas (pdf-extract, pdf-generate, library-ingest, suggestions, aurora-pgvector-init bootstrap), Bedrock KB (Aurora pgvector store) | | `proposal-system-frontend` | CloudFront distribution (S3 OAC) | | Resource Type | Names | |---|---| | S3 Buckets | `proposal-system-uploads`, `proposal-system-generated`, `proposal-system-library`, `seahaven-ios-certificates` | | SQS | `proposal-system-jobs` (720s visibility, SQS-managed encryption, reportBatchItemFailures) + `proposal-system-jobs-dlq` (SQS-managed encryption, message body filtering by jobType) | | Secrets | `proposal-system/db-credentials`, `proposal-system/internal-api-key` | ## Local Development ### Prerequisites - .NET 8 SDK - Node.js 24+ - Python 3.12 - PostgreSQL 16 (via docker-compose or native) ### Database ```bash docker compose up -d # starts PostgreSQL on port 5432 # database: proposalsystem, password: localdev ``` ### API ```bash cd api dotnet restore dotnet run --project src/ProposalSystem.Api # runs on http://localhost:5000 ``` In development mode (`DevMode=true` in appsettings.Development.json): - JWT auth uses a local symmetric HMAC key (no Cognito required) - S3 service returns fake presigned URLs - SQS publisher logs messages without sending ### Web Frontend ```bash cd web npm install npm run dev # runs on http://localhost:5173, proxies /api to localhost:5000 ``` When `VITE_COGNITO_CLIENT_ID` is not set, the login screen shows role-selector buttons for local development. ### Infrastructure ```bash cd infra npm install npx cdk synth ``` ## CI/CD ### CI (on pull request to main) Six parallel jobs calling org reusable workflows: | Job | Workflow | What it checks | |---|---|---| | .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (xUnit) | | Web Frontend Check | `ci-typescript-frontend.yaml` | Prettier `format:check`, build (includes `tsc -b`), vitest suite, Playwright chromium smoke (dev-login → proposal list, API mocked) | | Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile | | Python Lint | `ci-python-sam.yaml` | ruff check + format on lambdas/ | | Python Tests | inline job | pytest suite (37 tests — pdf-generate, suggestions, library-ingest, internal API signing) | | CDK Synth | `ci-typescript-cdk.yaml` | Synthesize CDK stacks (includes .NET publish) | ### Deploy (manual) Auto-deploy on push to main is currently disabled (PR #167) — both deploy workflows run via `workflow_dispatch` from the Actions tab. `deploy.yaml` calls the `cd-cdk.yaml` reusable workflow: 1. Publishes .NET 8 API and Python Lambdas 2. Runs `cdk deploy --all` 3. Executes `scripts/post-deploy.sh` (builds web, syncs to S3, invalidates CloudFront) Deploy uses OIDC role `githubdeploy-proposal-system`. Concurrency group prevents parallel deploys. ### Mobile Deploy Workflow: `deploy-mobile.yaml` -- builds and uploads to TestFlight via `cd-mobile-ios.yaml` reusable workflow on `macos-26`. Trigger: **manual only** (`workflow_dispatch`) while the app is pre-V1, to save macOS runner cost. ## Mobile iOS The iOS app uses **Fastlane** with **match** for code signing. Certificates and profiles are stored in the `seahaven-ios-certificates` S3 bucket (versioning enabled, public access blocked). Build and upload to TestFlight is handled by the `cd-mobile-ios.yaml` reusable workflow. Required secrets: | Secret | Purpose | |---|---| | `AWS_DEPLOY_ROLE_ARN` | OIDC role for match S3 access | | `MATCH_PASSWORD` | Decryption passphrase for signing assets | | `ASC_KEY_ID` | App Store Connect API key ID | | `ASC_ISSUER_ID` | App Store Connect issuer | | `ASC_KEY_CONTENT` | App Store Connect API key (base64) | ## Authentication & Authorization Two-layer auth architecture with defense-in-depth: | Path | Authorizer | Authentication | |---|---|---| | External clients → API Gateway `/{proxy+}` | Cognito JWT authorizer (web + mobile client IDs) | .NET JWT middleware (ValidateAudience=true) | | `/api/health` | None (public) | None | | `/api/auth/callback`, `/api/auth/dev-login` | None (unauthenticated) | None (pre-auth endpoints) | | Internal Lambdas → Function URL | AWS_IAM (grantInvokeUrl) | Internal API key (`X-Internal-Api-Key` header, value from Secrets Manager) | **Role-based access:** Cognito groups (`dispatchers`, `admins`, `sysadmins`) map to API roles via `cognito:groups` claim. Dispatchers can only see their own proposals (ownership enforced in service layer). VendorProposals and GeneratedPdfs endpoints restricted to admins/sysadmins. **Internal API key:** Python Lambdas call the .NET API via a Lambda Function URL with AWS_IAM auth (bypasses API Gateway JWT check). The `InternalApiKeyMiddleware` validates the `X-Internal-Api-Key` header and assigns the `admins` role to the synthetic identity. Lambdas cache the API key from Secrets Manager with a 5-minute TTL. **Optimistic concurrency (ADR 0004):** proposal responses carry an opaque `rowVersion` token; mutations of the proposal aggregate (update, approve, return-to-review, send, revise, bulk line-item update) require `proposalVersion` in the body. Missing token → 422 `ProposalVersionRequired`, malformed → 422 `InvalidRowVersion`, stale → **409 `{ message, currentState }`** with the reloaded proposal embedded (unguarded races → 409 `{ status, message, code }`). Line-item create/delete are token-less but bump the aggregate version. Audit rows commit atomically with their mutation (stage-then-single-SaveChanges). **Deploy note for schema changes:** EF migrations auto-apply at API startup under a `pg_advisory_lock`. Before deploying a migration: take a manual RDS snapshot; additive-only migrations are backward-compatible with the previous Lambda version. Test the down-script against a snapshot-restored copy before any production rollback. ## Data Flow 1. Dispatcher submits proposal request (web or mobile) 2. API creates proposal record (with advisory-locked number generation), publishes SQS message 3. If vendor PDF attached: `pdf-extract` Lambda parses and structures data 4. Suggestions Lambda queries Bedrock KB for similar proposals, generates line items via Claude 5. Admin reviews/edits line items in pricing workspace 6. On approval: `pdf-generate` Lambda creates branded PDF 7. On send: `library-ingest` Lambda adds approved proposal to KB for future matching Failed SQS messages are reported via `batchItemFailures` and retried up to 3 times before moving to the DLQ. ## Testing 186 tests across three stacks, all run in CI on every PR: | Suite | Framework | Count | Coverage | |---|---|---|---| | .NET API | xUnit | 123 | State machine transitions, authorization attributes, middleware, validators, ProposalNumberGenerator, LineItemService state guards | | Web | vitest | 26 | ProtectedRoute, RoleGuard, API client interceptor (401 logout, token attachment) | | Python Lambdas | pytest | 37 | pdf-generate, suggestions, library-ingest handler contracts, internal API signing | ```bash cd api && dotnet test # .NET tests cd web && npm test # vitest cd lambdas && python -m pytest # pytest ``` ## Security Hardening applied across all layers (see AUDIT-REPORT.md for full details): - **Auth:** Cognito JWT validation with audience check, startup fails if auth not configured, DevMode gated to `IsDevelopment()` - **API:** FluentValidation on all DTOs, generic error responses (no stack traces or config leaks), structured audit logging with before/after diffs - **Function URL:** AWS_IAM auth + internal API key (two-layer defense) - **Infrastructure:** S3 `enforceSSL` + `BLOCK_ALL`, SQS managed encryption, Aurora in private subnets, Cognito optional TOTP MFA, API Gateway access logging - **Lambdas:** Prompt injection sanitization, PDF size limits, numeric validation on AI suggestions, S3 key sanitization, idempotent SQS processing - **CI/CD:** OIDC (no long-lived credentials), org reusable workflows (`@main`), `--require-approval broadening` on local deploys - **Web:** sessionStorage for tokens (not localStorage), error boundaries, role guards on all admin routes, 401 interceptor clears auth state