* fix(auth): require Google authentication in cloud mode Remove the public shared-key mechanism and fail closed on Google auth while adding submit-route throttling. * fix(auth): address review follow-ups Fail closed on whitespace-only Google configuration and centralize shared authentication behavior. * test(auth): use non-secret Google client fixture Make the public test identifier explicit so secret scanning does not misclassify it as an API key. * test(auth): avoid OAuth-shaped fixture Use a format-neutral audience value so secret scanning can distinguish the fixture from a real client identifier. * chore(security): suppress public OAuth fixture Document the scanner false positive without suppressing any runtime credential flow. |
||
|---|---|---|
| .github | ||
| .security-review | ||
| functions | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| config.json | ||
| README.md | ||
| requirements.txt | ||
| samconfig.toml.example | ||
| slack-app-manifest.yml | ||
| template.yaml | ||
meal-order-manager
Automates weekly meal ordering from Redefine Meals for Sea Haven Industries employees. Scrapes the menu, generates an order form, collects individual orders, and produces payroll deduction reports plus a per-person weekly summary PDF.
Architecture
Monday 7:30am ET Employees (Mon–Thu) Thursday 6pm ET
┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ GitHub Actions │ │ orders.seahaven │ │ EventBridge │
│ - Scrape menu │────S3 upload───▶│ ind.com │ │ - Close form │
│ - Generate form │ + DynamoDB │ (CloudFront+S3) │──POST───┐ │ - Aggregate │
│ - Slack notify │ └──────────────────┘ │ │ - Slack summary │
└─────────────────┘ ▼ └──────────────────┘
┌──────────┐
Monday 7am ET │ API GW + │
┌──────────────────┐ │ Lambda │
│ EventBridge │ │ submit │
│ - Email payroll │ └────┬─────┘
│ deductions │ ▼
└──────────────────┘ ┌──────────┐
│ DynamoDB │
Thu 10am: Slack DM │ orders │
reminders to employees └──────────┘
who haven't ordered
Weekly Flow
| When | What | How |
|---|---|---|
| Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB |
| Monday 7am ET | Email previous week's payroll deductions to payroll@ |
EventBridge → Lambda → SES |
| Monday 7:30am ET | Scrape menu, generate form, upload to S3, post link to Slack | GitHub Actions cron |
| Mon–Thu | Employees visit orders.seahaven.com and submit orders |
S3 static form → API Gateway → Lambda → DynamoDB |
| Thursday 10am ET | DM employees who haven't ordered yet | EventBridge → Lambda → Slack DM |
| Thursday 6pm ET | Close form, aggregate orders, write CSV reports + weekly summary PDF, post Redefine order summary to Slack | EventBridge → Lambda chain |
Reports (written to meal-order-manager-reports-* at Thursday close)
| Key | Contents |
|---|---|
reports/{week}/order-summary.csv |
Meal-level aggregate (meal, qty, unit price, line total) for the Redefine order |
reports/{week}/payroll-deductions.csv |
Per-employee payroll deduction totals |
reports/{week}/weekly-summary-{week}.pdf |
Per-person summary (employee → item → quantity, no pricing); downloadable from the admin panel via presigned URL |
AWS Resources
Stack name: meal-order-manager (us-east-1)
- S3 —
meal-order-manager-form-*(static form hosting),meal-order-manager-reports-*(CSV reports + weekly summary PDF) - CloudFront — HTTPS distribution with custom domain
orders.seahaven.com - DynamoDB —
meal-order-manager-orders(orders, menu, roster, config) - API Gateway — HttpApi for order submission and admin operations
- Lambda — 7 functions: submit-order, admin-authorizer, close-form, aggregate-orders, slack-notifier, sync-roster, email-report
- EventBridge — scheduled rules (dual EST/EDT) for close, reminders, payroll email
- Secrets Manager — Slack bot token
- Migration note: the existing
meal-order-manager/form-api-keysecret remains until this change is deployed and verified, then must be deleted during post-deploy cleanup.
- Migration note: the existing
- SES — payroll deduction emails
- CloudWatch Alarms — coverage across the stack, all notifying the shared
site-alertsSNS topic (see Monitoring)
Monitoring
CloudWatch alarms are defined in template.yaml. Every alarm sends to the shared
site-alerts SNS topic (arn:aws:sns:us-east-1:328440206208:site-alerts), has no
OKActions, and treats missing data as not breaching (so idle/cron functions don't
sit in ALARM between runs). Alarm names follow meal-order-manager-<fn>-<signal>.
- Lambda Errors / Throttles — one alarm each per function (7 functions), Sum over 5 min, fires on any error/throttle (threshold 0).
- Lambda Duration — p99 over 5 min at ~80% of each function's timeout. API-fronted functions (submit-order, admin-authorizer) evaluate 3/3 datapoints; cron/async functions evaluate a single datapoint.
- DynamoDB orders table —
ReadThrottleEventsandWriteThrottleEvents(TableName dimension). DynamoDB does not publishThrottledRequests/SystemErrorsat the table-only dimension, so those are intentionally not alarmed. - API Gateway (OrderApi, HTTP API v2) — 5xx (threshold 0), 4xx (threshold 20, 3/2 datapoints to absorb routine 401s from the token authorizer), and p99 Latency (~3000ms). The submit route is limited to 5 requests/second with a burst of 10.
Authentication
Google Identity Services (OAuth) with tokeninfo endpoint verification. Accepts both seahavenind.com and seahaven.com Google Workspace domains. Cloud form generation and Lambda order submission fail closed unless /meal-order-manager/google-client-id is configured. The local Flask workflow can still use manual name and email entry when Google auth is not configured.
Admin Panel
Admins (configured in DynamoDB CONFIG/SETTINGS → admin_emails list) get an "Admin" button after Google sign-in. The panel provides:
- View all orders by week with totals
- Edit order quantities, add new menu items, remove items
- Delete orders entirely
- Download order list — a CSV rollup of item → total quantity across all employees (no per-employee breakdown, no prices) to drive the bulk Redefine order. Generated client-side from the loaded week, so it works for open weeks too.
- Download summary PDF — fetches a short-lived presigned URL for the week's per-person summary PDF (generated at Thursday close) and opens it. Returns 404 for weeks that haven't closed yet.
All admin operations enforce server-side price recalculation from the menu.
API routes (all require Google auth + admin email):
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/orders |
List weeks with order counts |
| GET | /api/admin/orders?week=YYYY-WNN |
Get all orders for a week |
| PUT | /api/admin/orders |
Update an order (recalculates prices) |
| DELETE | /api/admin/orders?week=...&email=... |
Delete an order |
| GET | /api/admin/summary-pdf?week=YYYY-WNN |
Presigned URL for the week's summary PDF (404 if week not closed) |
Documentation
The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's meal-order-manager stack is represented there as a Mermaid subgraph.
- AWS Architecture Map (Confluence, IT space, page 1540098)
Setup
Local development
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
Deploy to AWS
cp samconfig.toml.example samconfig.toml
# Edit samconfig.toml with your certificate ARN, etc.
sam build
sam deploy
Post-deploy
- Create the Slack bot token secret:
aws secretsmanager create-secret --name meal-order-manager/slack-bot-token --secret-string "xoxb-..." - Set the Google OAuth client ID:
aws ssm put-parameter --name /meal-order-manager/google-client-id --type String --value "<YOUR_GOOGLE_CLIENT_ID>" --overwrite - Update the Slack channel SSM parameter:
aws ssm put-parameter --name /meal-order-manager/slack-channel-id --value "C0XXXXXXX" --overwrite - Verify SES sender identity for
adam@seahavenind.com - Set up DNS: CNAME
orders.seahaven.com→ CloudFront distribution domain - Roster syncs automatically from Slack channel members (runs Monday 6:55am ET), or seed manually:
python3 scripts/seed_roster.py
Local Workflow (no AWS)
The scraper, form generator, Flask server, and aggregator still work locally:
python3 src/scraper/scrape_menu.py # scrape menu
python3 src/server/generate_form.py # generate form (local mode)
python3 src/server/app.py # serve on localhost:5050
python3 src/aggregator/aggregate.py # generate CSV reports
Configuration
config.json (local dev):
menu_url— Redefine Meals menu URLorder_deadline— displayed on the formroster— employee list (name, email, slack_user_id)google_client_id— optional locally; required for cloud generationoutput_dir/orders_dir— local output paths
Project Structure
meal-order-manager/
├── .github/workflows/
│ ├── weekly-menu.yml # Monday cron: scrape + publish + notify
│ ├── ci.yml # PR checks
│ └── deploy.yml # Push to main: sam deploy
├── src/
│ ├── scraper/ # Playwright menu scraper
│ ├── server/ # Form generator + local Flask server
│ ├── aggregator/ # Order aggregation + CSV reports
│ └── shared/shared/ # Lambda layer (db, secrets, slack, pdf helpers)
├── functions/ # Lambda handlers
│ ├── submit_order/
│ ├── close_form/
│ ├── aggregate_orders/
│ ├── slack_notifier/
│ ├── sync_roster/
│ └── email_report/
├── scripts/ # CI/CD helper scripts
│ ├── upload_menu.py
│ ├── notify_slack.py
│ └── seed_roster.py
├── template.yaml # SAM template
├── samconfig.toml.example
├── slack-app-manifest.yml # Slack app manifest (paste into api.slack.com)
└── config.json