2026-05-12 18:25:15 -04:00
|
|
|
AWSTemplateFormatVersion: '2010-09-09'
|
|
|
|
|
Transform: AWS::Serverless-2016-10-31
|
|
|
|
|
Description: >
|
|
|
|
|
meal-order-manager — automated weekly meal ordering from Redefine Meals
|
|
|
|
|
with employee order collection, Slack notifications, and payroll deduction reports.
|
|
|
|
|
|
|
|
|
|
Parameters:
|
|
|
|
|
CustomDomain:
|
|
|
|
|
Type: String
|
2026-05-12 20:16:46 -04:00
|
|
|
Default: orders.seahaven.com
|
2026-05-12 18:25:15 -04:00
|
|
|
Description: Custom domain for the order form (requires ACM cert)
|
|
|
|
|
CertificateArn:
|
|
|
|
|
Type: String
|
|
|
|
|
Default: ''
|
|
|
|
|
Description: ACM certificate ARN for the custom domain (us-east-1)
|
2026-06-02 17:20:58 -04:00
|
|
|
# Shared CloudFront WAF WebACL ARN (audit M-17), published to SSM by
|
|
|
|
|
# seahaven-account-baseline. Resolved at deploy time.
|
|
|
|
|
WebAclArn:
|
|
|
|
|
Type: AWS::SSM::Parameter::Value<String>
|
|
|
|
|
Default: /seahaven/waf/app-web-acl-arn
|
|
|
|
|
Description: ARN of the shared seahaven-app-waf CloudFront WebACL
|
2026-05-12 18:25:15 -04:00
|
|
|
Conditions:
|
|
|
|
|
HasCustomDomain: !Not [!Equals [!Ref CertificateArn, '']]
|
|
|
|
|
|
|
|
|
|
Globals:
|
|
|
|
|
Function:
|
|
|
|
|
Runtime: python3.12
|
|
|
|
|
Architectures:
|
|
|
|
|
- arm64
|
|
|
|
|
Timeout: 30
|
|
|
|
|
MemorySize: 256
|
2026-06-10 14:14:54 -04:00
|
|
|
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
|
2026-05-12 18:25:15 -04:00
|
|
|
Environment:
|
|
|
|
|
Variables:
|
|
|
|
|
TABLE_NAME: !Ref OrdersTable
|
|
|
|
|
REPORTS_BUCKET: !Ref ReportsBucket
|
|
|
|
|
SLACK_CHANNEL_PARAM: /meal-order-manager/slack-channel-id
|
|
|
|
|
FORM_URL: !If
|
|
|
|
|
- HasCustomDomain
|
|
|
|
|
- !Sub 'https://${CustomDomain}'
|
|
|
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
SLACK_BOT_SM_NAME: meal-order-manager/slack-bot-token
|
2026-05-12 18:25:15 -04:00
|
|
|
Layers:
|
|
|
|
|
- !Ref SharedLayer
|
|
|
|
|
|
|
|
|
|
Resources:
|
|
|
|
|
|
|
|
|
|
# ─── Shared Layer ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
SharedLayer:
|
|
|
|
|
Type: AWS::Serverless::LayerVersion
|
|
|
|
|
Properties:
|
|
|
|
|
LayerName: meal-order-manager-shared
|
|
|
|
|
ContentUri: src/shared/
|
|
|
|
|
CompatibleRuntimes:
|
|
|
|
|
- python3.12
|
|
|
|
|
CompatibleArchitectures:
|
|
|
|
|
- arm64
|
|
|
|
|
Metadata:
|
|
|
|
|
BuildMethod: python3.12
|
|
|
|
|
BuildArchitecture: arm64
|
|
|
|
|
|
|
|
|
|
# ─── DynamoDB ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
OrdersTable:
|
|
|
|
|
Type: AWS::DynamoDB::Table
|
|
|
|
|
Properties:
|
|
|
|
|
TableName: meal-order-manager-orders
|
|
|
|
|
BillingMode: PAY_PER_REQUEST
|
|
|
|
|
AttributeDefinitions:
|
|
|
|
|
- AttributeName: PK
|
|
|
|
|
AttributeType: S
|
|
|
|
|
- AttributeName: SK
|
|
|
|
|
AttributeType: S
|
|
|
|
|
KeySchema:
|
|
|
|
|
- AttributeName: PK
|
|
|
|
|
KeyType: HASH
|
|
|
|
|
- AttributeName: SK
|
|
|
|
|
KeyType: RANGE
|
|
|
|
|
TimeToLiveSpecification:
|
|
|
|
|
AttributeName: ttl
|
|
|
|
|
Enabled: true
|
|
|
|
|
|
|
|
|
|
# ─── S3 Buckets ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
FormBucket:
|
|
|
|
|
Type: AWS::S3::Bucket
|
|
|
|
|
Properties:
|
|
|
|
|
BucketName: !Sub 'meal-order-manager-form-${AWS::AccountId}'
|
|
|
|
|
PublicAccessBlockConfiguration:
|
|
|
|
|
BlockPublicAcls: true
|
|
|
|
|
BlockPublicPolicy: true
|
|
|
|
|
IgnorePublicAcls: true
|
|
|
|
|
RestrictPublicBuckets: true
|
|
|
|
|
LifecycleConfiguration:
|
|
|
|
|
Rules:
|
|
|
|
|
- Id: delete-old-archives
|
|
|
|
|
Prefix: archive/
|
|
|
|
|
Status: Enabled
|
|
|
|
|
ExpirationInDays: 90
|
|
|
|
|
Tags:
|
|
|
|
|
- Key: Purpose
|
|
|
|
|
Value: meal-order-form-hosting
|
|
|
|
|
- Key: ManagedBy
|
|
|
|
|
Value: meal-order-manager
|
|
|
|
|
|
|
|
|
|
FormBucketPolicy:
|
|
|
|
|
Type: AWS::S3::BucketPolicy
|
|
|
|
|
Properties:
|
|
|
|
|
Bucket: !Ref FormBucket
|
|
|
|
|
PolicyDocument:
|
|
|
|
|
Version: '2012-10-17'
|
|
|
|
|
Statement:
|
|
|
|
|
- Sid: AllowCloudFrontOAC
|
|
|
|
|
Effect: Allow
|
|
|
|
|
Principal:
|
|
|
|
|
Service: cloudfront.amazonaws.com
|
|
|
|
|
Action: s3:GetObject
|
|
|
|
|
Resource: !Sub '${FormBucket.Arn}/*'
|
|
|
|
|
Condition:
|
|
|
|
|
StringEquals:
|
|
|
|
|
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${FormDistribution}'
|
|
|
|
|
|
|
|
|
|
ReportsBucket:
|
|
|
|
|
Type: AWS::S3::Bucket
|
|
|
|
|
Properties:
|
|
|
|
|
BucketName: !Sub 'meal-order-manager-reports-${AWS::AccountId}'
|
|
|
|
|
PublicAccessBlockConfiguration:
|
|
|
|
|
BlockPublicAcls: true
|
|
|
|
|
BlockPublicPolicy: true
|
|
|
|
|
IgnorePublicAcls: true
|
|
|
|
|
RestrictPublicBuckets: true
|
|
|
|
|
LifecycleConfiguration:
|
|
|
|
|
Rules:
|
|
|
|
|
- Id: archive-old-reports
|
|
|
|
|
Status: Enabled
|
|
|
|
|
Transitions:
|
|
|
|
|
- StorageClass: GLACIER_IR
|
|
|
|
|
TransitionInDays: 90
|
|
|
|
|
Tags:
|
|
|
|
|
- Key: Purpose
|
|
|
|
|
Value: meal-order-reports
|
|
|
|
|
- Key: ManagedBy
|
|
|
|
|
Value: meal-order-manager
|
|
|
|
|
|
|
|
|
|
# ─── CloudFront ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
FormOAC:
|
|
|
|
|
Type: AWS::CloudFront::OriginAccessControl
|
|
|
|
|
Properties:
|
|
|
|
|
OriginAccessControlConfig:
|
|
|
|
|
Name: meal-order-manager-oac
|
|
|
|
|
OriginAccessControlOriginType: s3
|
|
|
|
|
SigningBehavior: always
|
|
|
|
|
SigningProtocol: sigv4
|
|
|
|
|
|
|
|
|
|
FormDistribution:
|
|
|
|
|
Type: AWS::CloudFront::Distribution
|
|
|
|
|
Properties:
|
|
|
|
|
DistributionConfig:
|
|
|
|
|
Enabled: true
|
|
|
|
|
DefaultRootObject: index.html
|
|
|
|
|
Comment: meal-order-manager form hosting
|
|
|
|
|
PriceClass: PriceClass_100
|
|
|
|
|
HttpVersion: http2and3
|
2026-06-02 17:20:58 -04:00
|
|
|
WebACLId: !Ref WebAclArn # shared CloudFront WAF (audit M-17)
|
2026-05-12 18:25:15 -04:00
|
|
|
Aliases: !If
|
|
|
|
|
- HasCustomDomain
|
|
|
|
|
- [!Ref CustomDomain]
|
|
|
|
|
- !Ref AWS::NoValue
|
|
|
|
|
ViewerCertificate: !If
|
|
|
|
|
- HasCustomDomain
|
|
|
|
|
- AcmCertificateArn: !Ref CertificateArn
|
|
|
|
|
SslSupportMethod: sni-only
|
|
|
|
|
MinimumProtocolVersion: TLSv1.2_2021
|
|
|
|
|
- CloudFrontDefaultCertificate: true
|
|
|
|
|
Origins:
|
|
|
|
|
- Id: S3FormOrigin
|
|
|
|
|
DomainName: !GetAtt FormBucket.RegionalDomainName
|
|
|
|
|
OriginAccessControlId: !Ref FormOAC
|
|
|
|
|
S3OriginConfig:
|
|
|
|
|
OriginAccessIdentity: ''
|
|
|
|
|
DefaultCacheBehavior:
|
|
|
|
|
TargetOriginId: S3FormOrigin
|
|
|
|
|
ViewerProtocolPolicy: redirect-to-https
|
|
|
|
|
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
|
|
|
|
|
Compress: true
|
|
|
|
|
AllowedMethods:
|
|
|
|
|
- GET
|
|
|
|
|
- HEAD
|
|
|
|
|
CachedMethods:
|
|
|
|
|
- GET
|
|
|
|
|
- HEAD
|
|
|
|
|
CustomErrorResponses:
|
|
|
|
|
- ErrorCode: 403
|
|
|
|
|
ResponseCode: 200
|
|
|
|
|
ResponsePagePath: /index.html
|
|
|
|
|
|
|
|
|
|
# ─── API Gateway ───────────────────────────────────────────────
|
|
|
|
|
|
2026-06-02 17:01:19 -04:00
|
|
|
ApiAccessLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: /aws/apigateway/meal-order-manager
|
|
|
|
|
RetentionInDays: 90
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
OrderApi:
|
|
|
|
|
Type: AWS::Serverless::HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
StageName: $default
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
# Gateway-level auth for /api/admin/* routes (INFRA-100). A Lambda
|
|
|
|
|
# authorizer validates the same Google ID token (Authorization: Bearer)
|
|
|
|
|
# the admin panel already sends, so admin routes are no longer
|
|
|
|
|
# AuthorizationType NONE. Public routes (submit-order, form-status,
|
2026-08-03 14:27:59 -04:00
|
|
|
# roster) stay open, while weekly-menu publication routes opt into IAM.
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Auth:
|
2026-08-03 14:27:59 -04:00
|
|
|
EnableIamAuthorizer: true
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Authorizers:
|
|
|
|
|
AdminGoogleAuthorizer:
|
|
|
|
|
FunctionArn: !GetAtt AdminAuthorizerFunction.Arn
|
|
|
|
|
FunctionInvokeRole: !GetAtt AdminAuthorizerInvokeRole.Arn
|
|
|
|
|
Identity:
|
|
|
|
|
Headers:
|
|
|
|
|
- Authorization
|
|
|
|
|
AuthorizerPayloadFormatVersion: '2.0'
|
|
|
|
|
EnableSimpleResponses: true
|
|
|
|
|
# Disable result caching: with caching, an expired Google token or
|
|
|
|
|
# an admin removed from admin_emails would stay authorized for the
|
|
|
|
|
# cache TTL. The tokeninfo call is the dominant latency anyway.
|
|
|
|
|
AuthorizerResultTtlInSeconds: 0
|
|
|
|
|
# No DefaultAuthorizer — routes opt in individually so the public
|
|
|
|
|
# routes remain unauthenticated.
|
2026-06-02 17:01:19 -04:00
|
|
|
# Access logging + default throttling (audit M-18).
|
|
|
|
|
AccessLogSettings:
|
|
|
|
|
DestinationArn: !GetAtt ApiAccessLogGroup.Arn
|
|
|
|
|
Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","requestTime":"$context.requestTime","method":"$context.httpMethod","routeKey":"$context.routeKey","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}'
|
|
|
|
|
DefaultRouteSettings:
|
|
|
|
|
ThrottlingBurstLimit: 50
|
|
|
|
|
ThrottlingRateLimit: 100
|
2026-08-03 13:51:12 -04:00
|
|
|
RouteSettings:
|
|
|
|
|
'POST /api/submit-order':
|
|
|
|
|
ThrottlingBurstLimit: 10
|
|
|
|
|
ThrottlingRateLimit: 5
|
2026-08-03 14:27:59 -04:00
|
|
|
'POST /api/publish/menu':
|
|
|
|
|
ThrottlingBurstLimit: 2
|
|
|
|
|
ThrottlingRateLimit: 1
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
# CORS only allows the production domain. For local development, use the
|
|
|
|
|
# Flask dev server (app.py) which proxies API requests and doesn't enforce CORS.
|
2026-05-12 18:25:15 -04:00
|
|
|
CorsConfiguration:
|
|
|
|
|
AllowOrigins:
|
|
|
|
|
- !If
|
|
|
|
|
- HasCustomDomain
|
|
|
|
|
- !Sub 'https://${CustomDomain}'
|
|
|
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
|
|
|
AllowMethods:
|
|
|
|
|
- GET
|
|
|
|
|
- POST
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
- PUT
|
|
|
|
|
- DELETE
|
2026-05-12 18:25:15 -04:00
|
|
|
- OPTIONS
|
|
|
|
|
AllowHeaders:
|
|
|
|
|
- Content-Type
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
- Authorization
|
2026-05-12 18:25:15 -04:00
|
|
|
MaxAge: 3600
|
|
|
|
|
|
|
|
|
|
# ─── Lambda Functions ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
SubmitOrderFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-submit-order
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/submit_order/
|
|
|
|
|
MemorySize: 128
|
|
|
|
|
Timeout: 10
|
|
|
|
|
Environment:
|
|
|
|
|
Variables:
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
|
GOOGLE_CLIENT_ID_PARAM: /meal-order-manager/google-client-id
|
2026-06-05 18:41:55 -04:00
|
|
|
REPORTS_BUCKET: !Ref ReportsBucket
|
2026-05-12 18:25:15 -04:00
|
|
|
Policies:
|
|
|
|
|
- DynamoDBCrudPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
2026-05-12 19:21:47 -04:00
|
|
|
- Statement:
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
- Effect: Allow
|
|
|
|
|
Action: lambda:InvokeFunction
|
|
|
|
|
Resource: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: ssm:GetParameter
|
|
|
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
2026-06-05 18:44:25 -04:00
|
|
|
# Read-only access to weekly summary PDFs (only — not the
|
|
|
|
|
# payroll/order CSVs) for the admin summary-pdf presigned-URL
|
|
|
|
|
# endpoint.
|
2026-06-05 18:41:55 -04:00
|
|
|
- Effect: Allow
|
|
|
|
|
Action: s3:GetObject
|
2026-06-05 18:44:25 -04:00
|
|
|
Resource: !Sub '${ReportsBucket.Arn}/reports/*/weekly-summary-*.pdf'
|
2026-05-12 18:25:15 -04:00
|
|
|
Events:
|
|
|
|
|
SubmitOrder:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/submit-order
|
|
|
|
|
Method: POST
|
|
|
|
|
FormStatus:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/form-status/{week}
|
|
|
|
|
Method: GET
|
2026-05-12 20:16:46 -04:00
|
|
|
Roster:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/roster
|
|
|
|
|
Method: GET
|
2026-08-03 14:27:59 -04:00
|
|
|
PublishSettings:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/publish/settings
|
|
|
|
|
Method: GET
|
|
|
|
|
Auth:
|
|
|
|
|
Authorizer: AWS_IAM
|
|
|
|
|
PublishMenu:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/publish/menu
|
|
|
|
|
Method: POST
|
|
|
|
|
Auth:
|
|
|
|
|
Authorizer: AWS_IAM
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
AdminOrders:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/admin/orders
|
|
|
|
|
Method: GET
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Auth:
|
|
|
|
|
Authorizer: AdminGoogleAuthorizer
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
AdminOrdersUpdate:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/admin/orders
|
|
|
|
|
Method: PUT
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Auth:
|
|
|
|
|
Authorizer: AdminGoogleAuthorizer
|
Add admin panel, fix dual-domain auth, harden scrape schedule (#14)
* Add admin panel, fix dual-domain auth, harden weekly scrape schedule
Accept both seahavenind.com and seahaven.com Google Workspace domains
for employee sign-in. Add admin panel with order management (view by
week, edit quantities, add/remove items, delete orders) behind Google
auth + DynamoDB admin_emails allowlist. Shift weekly menu scrape from
8:00am to 7:30am ET and add timezone guard to prevent duplicate runs
from dual EST/EDT crons.
* Rename Secrets Manager env vars to avoid CI false positive
The reusable CI workflow greps for keywords like TOKEN and API_KEY in
Lambda environment variables. Our env vars hold Secrets Manager lookup
names, not actual secrets, but the heuristic matched the SM key name
meal-order-manager/slack-bot-token. Rename SLACK_BOT_TOKEN_SECRET to
SLACK_BOT_SM_NAME and FORM_API_KEY_SECRET to FORM_APIKEY_SM_NAME, and
reorder the Globals block so the value falls outside the grep window.
2026-05-19 16:32:19 -04:00
|
|
|
AdminOrdersDelete:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/admin/orders
|
|
|
|
|
Method: DELETE
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Auth:
|
|
|
|
|
Authorizer: AdminGoogleAuthorizer
|
2026-06-05 18:41:55 -04:00
|
|
|
AdminSummaryPdf:
|
|
|
|
|
Type: HttpApi
|
|
|
|
|
Properties:
|
|
|
|
|
ApiId: !Ref OrderApi
|
|
|
|
|
Path: /api/admin/summary-pdf
|
|
|
|
|
Method: GET
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
Auth:
|
|
|
|
|
Authorizer: AdminGoogleAuthorizer
|
|
|
|
|
|
|
|
|
|
# ─── Admin API Authorizer (INFRA-100) ─────────────────────────
|
|
|
|
|
# Lambda authorizer validating the Google ID token + admin-email allow-list
|
|
|
|
|
# for every /api/admin/* route. Mirrors submit_order's _verify_admin so the
|
|
|
|
|
# existing admin panel works unchanged.
|
|
|
|
|
|
|
|
|
|
AdminAuthorizerFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-admin-authorizer
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/admin_authorizer/
|
|
|
|
|
MemorySize: 128
|
|
|
|
|
Timeout: 10
|
|
|
|
|
Environment:
|
|
|
|
|
Variables:
|
|
|
|
|
GOOGLE_CLIENT_ID_PARAM: /meal-order-manager/google-client-id
|
|
|
|
|
Policies:
|
|
|
|
|
- DynamoDBReadPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
|
|
|
|
- Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: ssm:GetParameter
|
|
|
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
|
|
|
|
|
|
|
|
AdminAuthorizerLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${AdminAuthorizerFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
|
|
|
|
# IAM role API Gateway assumes to invoke the authorizer Lambda.
|
|
|
|
|
AdminAuthorizerInvokeRole:
|
|
|
|
|
Type: AWS::IAM::Role
|
|
|
|
|
Properties:
|
2026-06-10 14:14:54 -04:00
|
|
|
Path: /cfn-managed/
|
|
|
|
|
PermissionsBoundary: arn:aws:iam::328440206208:policy/seahaven-lambda-execution-boundary
|
Add gateway-level authorizer to admin API (INFRA-100) (#24)
The /api/admin/* routes (GET/PUT/DELETE /api/admin/orders and
GET /api/admin/summary-pdf) were AuthorizationType NONE, relying entirely on
the in-handler _verify_admin Google-token + admin-email check. This adds an
HTTP API Lambda authorizer that enforces the same check at the gateway, so
unauthenticated requests are rejected before reaching the integration.
- New admin_authorizer Lambda: validates the Authorization: Bearer Google ID
token (aud + allowed Workspace domain) and the admin_emails allow-list from
DynamoDB, returning the HTTP API simple response {isAuthorized}. Fails closed
on missing config, unavailable client ID, bad token, or DynamoDB error.
- OrderApi gains an AdminGoogleAuthorizer with result caching disabled
(AuthorizerResultTtlInSeconds: 0) so expired tokens / removed admins can't be
served from cache. Wired onto all four admin events; no DefaultAuthorizer, so
public routes (submit-order, form-status, roster) stay NONE.
- IAM role for API Gateway to invoke the authorizer; 60-day log group.
- 10 unit tests for the authorizer.
No client change: the admin panel already sends Authorization: Bearer
<google_id_token>. The in-handler _verify_admin check stays as defense-in-depth.
Cross-reviewed by GPT-4.1 (APPROVE-WITH-FIXES); both BLOCK items applied
(disable authorizer caching, fail-closed on DynamoDB error).
2026-06-08 18:05:09 -04:00
|
|
|
AssumeRolePolicyDocument:
|
|
|
|
|
Version: '2012-10-17'
|
|
|
|
|
Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Principal:
|
|
|
|
|
Service: apigateway.amazonaws.com
|
|
|
|
|
Action: sts:AssumeRole
|
|
|
|
|
Policies:
|
|
|
|
|
- PolicyName: invoke-admin-authorizer
|
|
|
|
|
PolicyDocument:
|
|
|
|
|
Version: '2012-10-17'
|
|
|
|
|
Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: lambda:InvokeFunction
|
|
|
|
|
Resource: !GetAtt AdminAuthorizerFunction.Arn
|
2026-05-12 18:25:15 -04:00
|
|
|
|
|
|
|
|
CloseFormFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-close-form
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/close_form/
|
|
|
|
|
MemorySize: 128
|
|
|
|
|
Timeout: 30
|
|
|
|
|
Policies:
|
|
|
|
|
- DynamoDBCrudPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
|
|
|
|
- Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: lambda:InvokeFunction
|
|
|
|
|
Resource: !GetAtt AggregateOrdersFunction.Arn
|
|
|
|
|
Environment:
|
|
|
|
|
Variables:
|
|
|
|
|
AGGREGATE_FUNCTION_ARN: !GetAtt AggregateOrdersFunction.Arn
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
# Both EST and EDT schedules fire every week year-round. The handler is
|
|
|
|
|
# idempotent, so the "wrong timezone" firing is a harmless no-op.
|
2026-05-12 18:25:15 -04:00
|
|
|
Events:
|
|
|
|
|
CloseEST:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
Schedule: cron(59 4 ? * FRI *)
|
|
|
|
|
Description: 'Close form Thursday 11:59pm EST (04:59 UTC Friday)'
|
2026-05-12 18:25:15 -04:00
|
|
|
Enabled: true
|
|
|
|
|
CloseEDT:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
Schedule: cron(59 3 ? * FRI *)
|
|
|
|
|
Description: 'Close form Thursday 11:59pm EDT (03:59 UTC Friday)'
|
2026-05-12 18:25:15 -04:00
|
|
|
Enabled: true
|
|
|
|
|
|
|
|
|
|
AggregateOrdersFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-aggregate-orders
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/aggregate_orders/
|
|
|
|
|
MemorySize: 256
|
|
|
|
|
Timeout: 60
|
|
|
|
|
Policies:
|
|
|
|
|
- DynamoDBCrudPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
|
|
|
|
- S3CrudPolicy:
|
|
|
|
|
BucketName: !Ref ReportsBucket
|
|
|
|
|
- Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: lambda:InvokeFunction
|
|
|
|
|
Resource: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
|
Environment:
|
|
|
|
|
Variables:
|
|
|
|
|
SLACK_NOTIFIER_ARN: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
|
|
|
|
|
|
SlackNotifierFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-slack-notifier
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/slack_notifier/
|
|
|
|
|
MemorySize: 128
|
|
|
|
|
Timeout: 30
|
|
|
|
|
Policies:
|
|
|
|
|
- DynamoDBReadPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
|
|
|
|
- Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: secretsmanager:GetSecretValue
|
|
|
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: ssm:GetParameter
|
|
|
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
|
|
|
Events:
|
|
|
|
|
ReminderEST:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
|
|
|
|
Schedule: cron(0 15 ? * THU *)
|
|
|
|
|
Description: 'DM reminders Thursday 10am EST (15:00 UTC)'
|
|
|
|
|
Enabled: true
|
|
|
|
|
Input: '{"event": "reminder"}'
|
|
|
|
|
ReminderEDT:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
|
|
|
|
Schedule: cron(0 14 ? * THU *)
|
|
|
|
|
Description: 'DM reminders Thursday 10am EDT (14:00 UTC)'
|
|
|
|
|
Enabled: true
|
|
|
|
|
Input: '{"event": "reminder"}'
|
|
|
|
|
|
2026-05-12 19:21:47 -04:00
|
|
|
SyncRosterFunction:
|
|
|
|
|
Type: AWS::Serverless::Function
|
|
|
|
|
Properties:
|
|
|
|
|
FunctionName: meal-order-manager-sync-roster
|
|
|
|
|
Handler: handler.lambda_handler
|
|
|
|
|
CodeUri: functions/sync_roster/
|
|
|
|
|
MemorySize: 128
|
|
|
|
|
Timeout: 60
|
|
|
|
|
Policies:
|
|
|
|
|
- DynamoDBCrudPolicy:
|
|
|
|
|
TableName: !Ref OrdersTable
|
|
|
|
|
- Statement:
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: secretsmanager:GetSecretValue
|
|
|
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
|
|
|
- Effect: Allow
|
|
|
|
|
Action: ssm:GetParameter
|
|
|
|
|
Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/meal-order-manager/*'
|
|
|
|
|
Events:
|
|
|
|
|
SyncEST:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
|
|
|
|
Schedule: cron(55 11 ? * MON *)
|
|
|
|
|
Description: 'Sync roster Monday 6:55am EST (11:55 UTC) — before menu publish'
|
|
|
|
|
Enabled: true
|
|
|
|
|
SyncEDT:
|
|
|
|
|
Type: Schedule
|
|
|
|
|
Properties:
|
|
|
|
|
Schedule: cron(55 10 ? * MON *)
|
|
|
|
|
Description: 'Sync roster Monday 6:55am EDT (10:55 UTC) — before menu publish'
|
|
|
|
|
Enabled: true
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
# ─── CloudWatch Log Groups (60-day retention) ──────────────────
|
|
|
|
|
|
|
|
|
|
SubmitOrderLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${SubmitOrderFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
|
|
|
|
CloseFormLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${CloseFormFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
|
|
|
|
AggregateOrdersLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${AggregateOrdersFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
|
|
|
|
SlackNotifierLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${SlackNotifierFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
2026-05-12 19:21:47 -04:00
|
|
|
SyncRosterLogGroup:
|
|
|
|
|
Type: AWS::Logs::LogGroup
|
|
|
|
|
Properties:
|
|
|
|
|
LogGroupName: !Sub '/aws/lambda/${SyncRosterFunction}'
|
|
|
|
|
RetentionInDays: 60
|
|
|
|
|
|
2026-06-17 14:45:54 -04:00
|
|
|
# ─── CloudWatch Alarms ─────────────────────────────────────────
|
|
|
|
|
# All alarms notify the shared site-alerts SNS topic. No OKActions
|
|
|
|
|
# (no recovery spam); TreatMissingData notBreaching so idle / cron
|
|
|
|
|
# functions don't sit in ALARM between invocations.
|
|
|
|
|
#
|
|
|
|
|
# Naming: meal-order-manager-<fn>-<signal> (repo-namespaced kebab-case).
|
|
|
|
|
#
|
|
|
|
|
# Duration alarms use ExtendedStatistic p99 at ~80% of each function's
|
|
|
|
|
# configured timeout. Synchronous (API-fronted) functions evaluate over
|
|
|
|
|
# 3 datapoints; cron / async-invoked functions evaluate a single datapoint
|
|
|
|
|
# (they fire too rarely for a multi-datapoint window).
|
|
|
|
|
|
|
|
|
|
# Lambda Errors (Sum, 5min, any error breaches)
|
|
|
|
|
SubmitOrderErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-submit-order-errors
|
|
|
|
|
AlarmDescription: submit-order Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SubmitOrderFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AdminAuthorizerErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-admin-authorizer-errors
|
|
|
|
|
AlarmDescription: admin-authorizer Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AdminAuthorizerFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
CloseFormErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-close-form-errors
|
|
|
|
|
AlarmDescription: close-form Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref CloseFormFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AggregateOrdersErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-aggregate-orders-errors
|
|
|
|
|
AlarmDescription: aggregate-orders Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AggregateOrdersFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SlackNotifierErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-slack-notifier-errors
|
|
|
|
|
AlarmDescription: slack-notifier Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SlackNotifierFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SyncRosterErrorsAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-sync-roster-errors
|
|
|
|
|
AlarmDescription: sync-roster Lambda reported one or more errors in 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Errors
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SyncRosterFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# Lambda Throttles (Sum, 5min, any throttle breaches)
|
|
|
|
|
SubmitOrderThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-submit-order-throttles
|
|
|
|
|
AlarmDescription: submit-order Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SubmitOrderFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AdminAuthorizerThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-admin-authorizer-throttles
|
|
|
|
|
AlarmDescription: admin-authorizer Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AdminAuthorizerFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
CloseFormThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-close-form-throttles
|
|
|
|
|
AlarmDescription: close-form Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref CloseFormFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AggregateOrdersThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-aggregate-orders-throttles
|
|
|
|
|
AlarmDescription: aggregate-orders Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AggregateOrdersFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SlackNotifierThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-slack-notifier-throttles
|
|
|
|
|
AlarmDescription: slack-notifier Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SlackNotifierFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SyncRosterThrottlesAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-sync-roster-throttles
|
|
|
|
|
AlarmDescription: sync-roster Lambda was throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Throttles
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SyncRosterFunction
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# Lambda Duration p99 (~80% of timeout)
|
|
|
|
|
# Synchronous (API-fronted) functions: eval 3 / datapoints 3.
|
|
|
|
|
SubmitOrderDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-submit-order-duration
|
|
|
|
|
AlarmDescription: submit-order p99 duration exceeded 8000ms (80% of 10s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SubmitOrderFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 3
|
|
|
|
|
DatapointsToAlarm: 3
|
|
|
|
|
Threshold: 8000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AdminAuthorizerDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-admin-authorizer-duration
|
|
|
|
|
AlarmDescription: admin-authorizer p99 duration exceeded 8000ms (80% of 10s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AdminAuthorizerFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 3
|
|
|
|
|
DatapointsToAlarm: 3
|
|
|
|
|
Threshold: 8000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# Cron / async-invoked functions: single datapoint (eval 1).
|
|
|
|
|
CloseFormDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-close-form-duration
|
|
|
|
|
AlarmDescription: close-form p99 duration exceeded 24000ms (80% of 30s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref CloseFormFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
DatapointsToAlarm: 1
|
|
|
|
|
Threshold: 24000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
AggregateOrdersDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-aggregate-orders-duration
|
|
|
|
|
AlarmDescription: aggregate-orders p99 duration exceeded 48000ms (80% of 60s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref AggregateOrdersFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
DatapointsToAlarm: 1
|
|
|
|
|
Threshold: 48000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SlackNotifierDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-slack-notifier-duration
|
|
|
|
|
AlarmDescription: slack-notifier p99 duration exceeded 24000ms (80% of 30s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SlackNotifierFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
DatapointsToAlarm: 1
|
|
|
|
|
Threshold: 24000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
SyncRosterDurationAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-sync-roster-duration
|
|
|
|
|
AlarmDescription: sync-roster p99 duration exceeded 48000ms (80% of 60s timeout).
|
|
|
|
|
Namespace: AWS/Lambda
|
|
|
|
|
MetricName: Duration
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: FunctionName
|
|
|
|
|
Value: !Ref SyncRosterFunction
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
DatapointsToAlarm: 1
|
|
|
|
|
Threshold: 48000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# DynamoDB orders table.
|
|
|
|
|
# NOTE: DynamoDB does NOT publish ThrottledRequests or SystemErrors at the
|
|
|
|
|
# TableName-only dimension (verified via cloudwatch list-metrics on
|
|
|
|
|
# 2026-06-17 — those metrics carry a TableName+Operation dimension pair and
|
|
|
|
|
# only on-occurrence). A TableName-dim alarm on them would never evaluate.
|
|
|
|
|
# The codifiable table-level throttle signals are ReadThrottleEvents and
|
|
|
|
|
# WriteThrottleEvents, which DO carry a TableName-only dimension. Those are
|
|
|
|
|
# used here for throttle coverage; a TableName-dim SystemErrors alarm is
|
|
|
|
|
# omitted (no such metric is emitted). See PR body.
|
|
|
|
|
OrdersTableReadThrottleAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-orders-read-throttle
|
|
|
|
|
AlarmDescription: orders table read requests were throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/DynamoDB
|
|
|
|
|
MetricName: ReadThrottleEvents
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: TableName
|
|
|
|
|
Value: !Ref OrdersTable
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
OrdersTableWriteThrottleAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-orders-write-throttle
|
|
|
|
|
AlarmDescription: orders table write requests were throttled in the last 5 minutes.
|
|
|
|
|
Namespace: AWS/DynamoDB
|
|
|
|
|
MetricName: WriteThrottleEvents
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: TableName
|
|
|
|
|
Value: !Ref OrdersTable
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# API Gateway (HTTP API v2) — OrderApi. Metrics carry the ApiId dimension.
|
|
|
|
|
OrderApi5xxAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-order-api-5xx
|
|
|
|
|
AlarmDescription: OrderApi returned one or more 5xx responses in 5 minutes.
|
|
|
|
|
Namespace: AWS/ApiGateway
|
|
|
|
|
MetricName: 5xx
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: ApiId
|
|
|
|
|
Value: !Ref OrderApi
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 1
|
|
|
|
|
Threshold: 0
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# 4xx threshold raised + eval 3 / dp 2 to absorb routine 401s from the
|
|
|
|
|
# token-based admin authorizer without paging.
|
|
|
|
|
OrderApi4xxAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-order-api-4xx
|
|
|
|
|
AlarmDescription: OrderApi 4xx responses exceeded 20 in 5 minutes (beyond routine auth noise).
|
|
|
|
|
Namespace: AWS/ApiGateway
|
|
|
|
|
MetricName: 4xx
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: ApiId
|
|
|
|
|
Value: !Ref OrderApi
|
|
|
|
|
Statistic: Sum
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 3
|
|
|
|
|
DatapointsToAlarm: 2
|
|
|
|
|
Threshold: 20
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
|
|
|
|
# Latency p99 ~3000ms (see PR body).
|
|
|
|
|
OrderApiLatencyAlarm:
|
|
|
|
|
Type: AWS::CloudWatch::Alarm
|
|
|
|
|
Properties:
|
|
|
|
|
AlarmName: meal-order-manager-order-api-latency
|
|
|
|
|
AlarmDescription: OrderApi p99 latency exceeded 3000ms.
|
|
|
|
|
Namespace: AWS/ApiGateway
|
|
|
|
|
MetricName: Latency
|
|
|
|
|
Dimensions:
|
|
|
|
|
- Name: ApiId
|
|
|
|
|
Value: !Ref OrderApi
|
|
|
|
|
ExtendedStatistic: p99
|
|
|
|
|
Period: 300
|
|
|
|
|
EvaluationPeriods: 3
|
|
|
|
|
DatapointsToAlarm: 3
|
|
|
|
|
Threshold: 3000
|
|
|
|
|
ComparisonOperator: GreaterThanThreshold
|
|
|
|
|
TreatMissingData: notBreaching
|
|
|
|
|
AlarmActions:
|
|
|
|
|
- arn:aws:sns:us-east-1:328440206208:site-alerts
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
# ─── SSM Parameters ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
SlackChannelParam:
|
|
|
|
|
Type: AWS::SSM::Parameter
|
|
|
|
|
Properties:
|
|
|
|
|
Name: /meal-order-manager/slack-channel-id
|
|
|
|
|
Type: String
|
|
|
|
|
Value: CHANGE_ME
|
|
|
|
|
Description: Slack channel ID for meal order notifications
|
|
|
|
|
|
Add discount pricing, Google auth, and order hardening (#10)
* Add discount settings and two-tier pricing to order aggregation
Settings CRUD (get_settings/put_settings) for DynamoDB CONFIG#SETTINGS item.
Aggregation now tracks bulk_price and employee_price separately, with
grand_total (company cost) and employee_total (payroll deductions).
* Add Google OAuth, server-side discounts, and Slack order confirmations
Submit order Lambda now verifies Google ID tokens via tokeninfo endpoint,
calculates two-tier discount pricing server-side, and async-invokes the
Slack notifier for per-employee order confirmation DMs. Deadlines updated
to Thursday 11:59pm across all Slack messages.
* Update SAM template for Google auth, Slack invocation, and deadline change
Add SLACK_NOTIFIER_ARN and GOOGLE_CLIENT_ID_PARAM env vars to submit order
function with lambda:InvokeFunction policy. Move close-form schedule to
Thursday 11:59pm EST/EDT.
* Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In
overlay, and closed-orders page with countdown timer. CI workflow fetches
discount settings from DynamoDB and Google Client ID from SSM.
* Add SSM GetParameter permission to submit order Lambda
Required for reading the Google Client ID from Parameter Store
at /meal-order-manager/google-client-id.
* Harden auth, pricing, and reliability in order handlers
Enforce Google auth when configured (reject missing tokens with 403),
return 503 on token verification outages, switch to Decimal with
ROUND_HALF_UP for financial precision, clamp discount bounds 0-100,
use email-based slugs, add 5-min cache TTL with time.monotonic(),
wrap Slack invocation in try/except, add reopen_at timestamp to
closed form status, add reminder dedup guards for dual EST/EDT crons,
escape Slack mrkdwn special characters, and handle empty employee names.
* Fix XSS risks and add closed-form UX to order page
Add escapeHtml() for all scraped content in innerHTML, fix script
injection via </script> in JSON, fix JWT base64url decoding, match
backend two-step rounding in JS employeePrice(), disable qty buttons
and submit when form is closed, add server-driven countdown from
reopen_at, add duplicate order warning via localStorage, add back
button after submission, embed favicon, use :g format for fractional
discounts, and exclude dead loadRoster code when Google auth enabled.
* Document CORS, cron idempotency, and SSM config in template
Add comments explaining CORS dev server strategy, dual EST/EDT cron
idempotency, and manual SSM parameter creation for Google Client ID.
* Add unit tests for submit, notify, and aggregate handlers
50 tests covering pricing pipeline (Decimal rounding, clamping, totals),
Google auth (enforcement, bypass prevention, audience/domain validation,
503 on outage), email slug generation, form status with reopen_at,
input validation, Slack failure resilience, reminder dedup guards,
order confirmation DMs, aggregated summaries, CSV generation, and
mrkdwn escaping.
* Use full email as order slug for defense-in-depth
Replace email-prefix slug with full lowercase email to eliminate any
possibility of cross-domain collisions, per senior review sign-off.
* Remove unused imports flagged by ruff
* Apply ruff formatting
* Fix PR review findings: auth, rounding, and close-form guard
- Remove dead elif branch in submit_order auth (always returned 403)
- Catch HTTPError before URLError so expired tokens return 403 not 503
- Wrap SSM get_parameter in try/except for fresh deployments
- Add wall-clock guard to close_form handler (Friday >= 11 PM ET)
- Add epsilon nudge to JS employeePrice for IEEE 754 boundary match
- Switch Flask dev server from round() to Decimal ROUND_HALF_UP
- Add tests for HTTPError handling and close_form guard (6 new tests)
* Fix close-form weekday guard and SSM auth fail-open
- Close form guard: check weekday == 3 (Thursday), not 4 (Friday) — the
crons fire at Thursday 11:59 PM ET, when weekday() is 3
- SSM fail-closed: separate _google_auth_configured() (checks env var) from
_get_google_client_id() (fetches value). If auth is configured but the SSM
fetch fails, return 503 instead of silently falling back to manual auth
- Update close_form tests to use Thursday dates
- Add test_ssm_failure_fails_closed
* Harden Flask dev server auth and escaping
- Add hosted domain check to _verify_google_token (mirror Lambda)
- Gate auth on config (client_id presence), not request body — prevents
bypass by omitting google_id_token when auth is configured
- Add discount percentage clamping to match Lambda handler
- Add </script> escaping to google_client_id_json
* fix: Email order filenames, SSM param TTL, DST-safe reopen_at
- Flask dev server: persist orders under lowercase email slug (match Lambda/Dynamo)
- shared.secrets: split secret vs SSM caches; expire get_parameter entries every 5 minutes
- form-status reopen_at: calendar Monday + datetime.combine for 8am ET (not 24h timedelta)
- Add _eastern_now() for testability; tests for SSM TTL and DST weekend edge case
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* Apply ruff formatting to submit_order handler
* fix(server): retry SSM for Google client id after TTL on failure
Transient SSM errors no longer cache empty client id for the process lifetime;
matches Lambda handler refresh behavior (300s TTL).
Co-authored-by: Cursor <cursoragent@cursor.com>
* style(server): ruff-format Google client id cache helper
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(close-form): accept Fri 00–03 ET catch-up after Thu close cron
EventBridge can deliver past midnight ET; widen the wall-clock guard so a
delayed Thursday 23:59 UTC cron still closes the form. Idempotent when already
closed. Adds test for early Friday; past-window skip now starts Fri 04:00 ET.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix(submit-order): bill from Dynamo menu retail, not client JSON
Load authoritative meal prices from get_menu(week); reject unknown meal names
and return 503 when the menu has no priced meals. Use meal_name in the pricing
loop to avoid shadowing the employee name. Adds regression tests for tampering,
unknown meals, and empty menu meals.
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
* fix: use single braces in loadRoster JS nested string
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix Eastern fallback countdown
* Fix pricing validation and JWT display decoding
* Fix optional Google auth detection
* Format app.py line length for ruff compliance
* Fix auth config check and URL escaping in form
- _google_auth_configured() now checks env var presence (intent), not
the fetched SSM value — prevents silent auth bypass if SSM param is
deleted
- Add </script> escaping to URL values in generate_form.py for
consistency with other injected values
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Adam Moussa <amoussa1229@users.noreply.github.com>
2026-05-13 18:00:21 -04:00
|
|
|
# GoogleClientIdParam (/meal-order-manager/google-client-id) is managed
|
|
|
|
|
# manually via AWS CLI since it varies per environment. Create it with:
|
|
|
|
|
# aws ssm put-parameter --name /meal-order-manager/google-client-id \
|
|
|
|
|
# --type String --value "<YOUR_GOOGLE_CLIENT_ID>"
|
|
|
|
|
|
2026-05-12 18:25:15 -04:00
|
|
|
Outputs:
|
|
|
|
|
ApiUrl:
|
|
|
|
|
Description: API Gateway endpoint URL
|
|
|
|
|
Value: !Sub 'https://${OrderApi}.execute-api.${AWS::Region}.amazonaws.com'
|
|
|
|
|
FormUrl:
|
|
|
|
|
Description: Order form URL
|
|
|
|
|
Value: !If
|
|
|
|
|
- HasCustomDomain
|
|
|
|
|
- !Sub 'https://${CustomDomain}'
|
|
|
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
|
|
|
DistributionId:
|
|
|
|
|
Description: CloudFront distribution ID (for cache invalidation)
|
|
|
|
|
Value: !Ref FormDistribution
|
|
|
|
|
FormBucketName:
|
|
|
|
|
Description: S3 bucket for form HTML
|
|
|
|
|
Value: !Ref FormBucket
|
|
|
|
|
ReportsBucketName:
|
|
|
|
|
Description: S3 bucket for CSV reports
|
|
|
|
|
Value: !Ref ReportsBucket
|
|
|
|
|
OrdersTableName:
|
|
|
|
|
Description: DynamoDB table name
|
|
|
|
|
Value: !Ref OrdersTable
|
2026-05-12 19:21:47 -04:00
|
|
|
SubmitOrderFunctionArn:
|
|
|
|
|
Description: Submit Order Lambda ARN
|
|
|
|
|
Value: !GetAtt SubmitOrderFunction.Arn
|
|
|
|
|
CloseFormFunctionArn:
|
|
|
|
|
Description: Close Form Lambda ARN
|
|
|
|
|
Value: !GetAtt CloseFormFunction.Arn
|
|
|
|
|
AggregateOrdersFunctionArn:
|
|
|
|
|
Description: Aggregate Orders Lambda ARN
|
|
|
|
|
Value: !GetAtt AggregateOrdersFunction.Arn
|
|
|
|
|
SlackNotifierFunctionArn:
|
|
|
|
|
Description: Slack Notifier Lambda ARN
|
|
|
|
|
Value: !GetAtt SlackNotifierFunction.Arn
|
|
|
|
|
SyncRosterFunctionArn:
|
|
|
|
|
Description: Sync Roster Lambda ARN
|
|
|
|
|
Value: !GetAtt SyncRosterFunction.Arn
|