mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 07:43:13 +00:00
- Add sync-roster Lambda that auto-syncs employee roster from Slack channel membership (runs Monday 6:55am ET before menu publish) - Move FormApiKey from CloudFormation parameter/env var to Secrets Manager (meal-order-manager/form-api-key) per security conventions - Add Slack app manifest with required bot scopes - Add get_channel_members() and get_user_info() to shared Slack module - Add Lambda function ARN outputs to CloudFormation - Add log group for sync-roster Lambda (60-day retention)
496 lines
16 KiB
YAML
496 lines
16 KiB
YAML
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
|
|
Default: orders.seahavenind.com
|
|
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)
|
|
PayrollEmail:
|
|
Type: String
|
|
Default: payroll@seahavenind.com
|
|
Description: Email address for payroll deduction reports
|
|
SenderEmail:
|
|
Type: String
|
|
Default: adam@seahavenind.com
|
|
Description: SES verified sender email for payroll reports
|
|
Conditions:
|
|
HasCustomDomain: !Not [!Equals [!Ref CertificateArn, '']]
|
|
|
|
Globals:
|
|
Function:
|
|
Runtime: python3.12
|
|
Architectures:
|
|
- arm64
|
|
Timeout: 30
|
|
MemorySize: 256
|
|
Environment:
|
|
Variables:
|
|
TABLE_NAME: !Ref OrdersTable
|
|
REPORTS_BUCKET: !Ref ReportsBucket
|
|
SLACK_BOT_TOKEN_SECRET: meal-order-manager/slack-bot-token
|
|
SLACK_CHANNEL_PARAM: /meal-order-manager/slack-channel-id
|
|
FORM_URL: !If
|
|
- HasCustomDomain
|
|
- !Sub 'https://${CustomDomain}'
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
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
|
|
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 ───────────────────────────────────────────────
|
|
|
|
OrderApi:
|
|
Type: AWS::Serverless::HttpApi
|
|
Properties:
|
|
StageName: $default
|
|
CorsConfiguration:
|
|
AllowOrigins:
|
|
- !If
|
|
- HasCustomDomain
|
|
- !Sub 'https://${CustomDomain}'
|
|
- !Sub 'https://${FormDistribution.DomainName}'
|
|
AllowMethods:
|
|
- GET
|
|
- POST
|
|
- OPTIONS
|
|
AllowHeaders:
|
|
- Content-Type
|
|
- x-api-key
|
|
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:
|
|
FORM_API_KEY_SECRET: meal-order-manager/form-api-key
|
|
Policies:
|
|
- DynamoDBCrudPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action: secretsmanager:GetSecretValue
|
|
Resource: !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:meal-order-manager/*'
|
|
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
|
|
|
|
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
|
|
Events:
|
|
CloseEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 23 ? * THU *)
|
|
Description: 'Close form Thursday 6pm EST (23:00 UTC)'
|
|
Enabled: true
|
|
CloseEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 22 ? * THU *)
|
|
Description: 'Close form Thursday 6pm EDT (22:00 UTC)'
|
|
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"}'
|
|
|
|
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
|
|
|
|
EmailReportFunction:
|
|
Type: AWS::Serverless::Function
|
|
Properties:
|
|
FunctionName: meal-order-manager-email-report
|
|
Handler: handler.lambda_handler
|
|
CodeUri: functions/email_report/
|
|
MemorySize: 128
|
|
Timeout: 30
|
|
Environment:
|
|
Variables:
|
|
PAYROLL_EMAIL: !Ref PayrollEmail
|
|
SENDER_EMAIL: !Ref SenderEmail
|
|
Policies:
|
|
- DynamoDBReadPolicy:
|
|
TableName: !Ref OrdersTable
|
|
- S3ReadPolicy:
|
|
BucketName: !Ref ReportsBucket
|
|
- Statement:
|
|
- Effect: Allow
|
|
Action:
|
|
- ses:SendRawEmail
|
|
Resource: '*'
|
|
Events:
|
|
PayrollEmailEST:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 12 ? * MON *)
|
|
Description: 'Email payroll deductions Monday 7am EST (12:00 UTC)'
|
|
Enabled: true
|
|
PayrollEmailEDT:
|
|
Type: Schedule
|
|
Properties:
|
|
Schedule: cron(0 11 ? * MON *)
|
|
Description: 'Email payroll deductions Monday 7am EDT (11:00 UTC)'
|
|
Enabled: true
|
|
|
|
# ─── 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
|
|
|
|
EmailReportLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${EmailReportFunction}'
|
|
RetentionInDays: 60
|
|
|
|
SyncRosterLogGroup:
|
|
Type: AWS::Logs::LogGroup
|
|
Properties:
|
|
LogGroupName: !Sub '/aws/lambda/${SyncRosterFunction}'
|
|
RetentionInDays: 60
|
|
|
|
# ─── 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
|
|
|
|
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
|
|
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
|
|
EmailReportFunctionArn:
|
|
Description: Email Report Lambda ARN
|
|
Value: !GetAtt EmailReportFunction.Arn
|