mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 03:03:12 +00:00
* feat(api): serve meals on ECS Fargate instead of Lambda Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway. * fix(jobs): run delayed close and reminder deliveries Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled. * fix(api): return JSON objects and stop logging job payloads Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status. * fix(ci): restore the reusable workflow so the required check is named ci / ci Inlining the job reported `ci` instead of the org ruleset's `ci / ci`. * fix(secrets): drop unused os import so ruff check passes * style: apply ruff format so ci-python-app lint passes * fix(infra): give meals its own VPC because prod has none * chore(security): re-key ALB SG checkov suppression after vpc.tf
133 lines
3.8 KiB
Python
133 lines
3.8 KiB
Python
"""Production Flask app for meal-order-manager.
|
|
|
|
Local: PYTHONPATH=src:src/shared python3 -m server.app
|
|
Prod: gunicorn server.wsgi:app
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import json
|
|
|
|
from flask import Flask, Response, jsonify, request, send_file
|
|
|
|
from server import http_api
|
|
|
|
CORS_ORIGINS = [
|
|
"https://orders.seahaven.com",
|
|
"https://internal.seahaven.com",
|
|
"https://internal.dev.seahaven.com",
|
|
"http://localhost:5173",
|
|
"http://localhost:4173",
|
|
"http://127.0.0.1:5173",
|
|
"http://127.0.0.1:5050",
|
|
]
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
OUTPUT_DIR = PROJECT_ROOT / "output"
|
|
|
|
|
|
def create_app() -> Flask:
|
|
app = Flask(__name__)
|
|
extra = os.environ.get("CORS_ORIGINS", "")
|
|
origins = list(CORS_ORIGINS)
|
|
if extra:
|
|
origins.extend(o.strip() for o in extra.split(",") if o.strip())
|
|
form_url = os.environ.get("FORM_URL", "").rstrip("/")
|
|
if form_url and form_url not in origins:
|
|
origins.append(form_url)
|
|
|
|
@app.after_request
|
|
def add_cors(resp: Response) -> Response:
|
|
origin = request.headers.get("Origin", "")
|
|
if origin in origins:
|
|
resp.headers["Access-Control-Allow-Origin"] = origin
|
|
resp.headers["Vary"] = "Origin"
|
|
resp.headers["Access-Control-Allow-Headers"] = (
|
|
"Authorization, Content-Type, X-Meals-Publish-Key"
|
|
)
|
|
resp.headers["Access-Control-Allow-Methods"] = (
|
|
"GET, POST, PUT, DELETE, OPTIONS"
|
|
)
|
|
resp.headers["Access-Control-Max-Age"] = "3600"
|
|
return resp
|
|
|
|
@app.route("/api/health")
|
|
def health():
|
|
return jsonify(
|
|
{
|
|
"stage": os.environ.get("STAGE", "local"),
|
|
"sha": os.environ.get("GIT_SHA", "dev"),
|
|
}
|
|
)
|
|
|
|
@app.route("/", methods=["GET"])
|
|
def root():
|
|
if os.environ.get("STAGE", "local") == "local":
|
|
from datetime import datetime
|
|
|
|
form_file = OUTPUT_DIR / (
|
|
f"order-form-{datetime.now().strftime('%Y-W%U')}.html"
|
|
)
|
|
if form_file.exists():
|
|
return send_file(form_file)
|
|
return "ok", 200
|
|
|
|
def _dispatch(path: str):
|
|
if request.method == "OPTIONS":
|
|
return "", 204
|
|
qs = request.args.to_dict(flat=True)
|
|
path_params = {}
|
|
parts = path.strip("/").split("/")
|
|
if (
|
|
len(parts) >= 3
|
|
and parts[0] == "api"
|
|
and parts[1]
|
|
in {
|
|
"menu",
|
|
"orders",
|
|
"form-status",
|
|
}
|
|
):
|
|
path_params["week"] = parts[2]
|
|
event = {
|
|
"requestContext": {"http": {"method": request.method, "path": path}},
|
|
"rawPath": path,
|
|
"headers": {k: v for k, v in request.headers.items()},
|
|
"body": request.get_data(as_text=True) or "{}",
|
|
"pathParameters": path_params,
|
|
"queryStringParameters": qs or None,
|
|
}
|
|
result = http_api.lambda_handler(event, None)
|
|
try:
|
|
payload = json.loads(result["body"])
|
|
except (TypeError, KeyError, json.JSONDecodeError):
|
|
resp = jsonify({"error": "Internal error"})
|
|
resp.status_code = 500
|
|
return resp
|
|
if not isinstance(payload, dict):
|
|
resp = jsonify({"error": "Internal error"})
|
|
resp.status_code = 500
|
|
return resp
|
|
resp = jsonify(payload)
|
|
resp.status_code = int(result.get("statusCode") or 500)
|
|
return resp
|
|
|
|
@app.route("/api/<path:rest>", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"])
|
|
def api(rest: str):
|
|
return _dispatch("/api/" + rest)
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
def main():
|
|
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "5050")))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|