meal-order-manager/src/server/generate_form.py
Adam Moussa 637b395982
fix(auth): address review follow-ups
Fail closed on whitespace-only Google configuration and centralize shared authentication behavior.
2026-07-31 11:49:09 -04:00

195 lines
6.2 KiB
Python

"""
Generates a self-contained HTML order form from the latest scraped menu.
The form shows this week's meals with quantity selectors, a running total,
and submits orders to the backend (local Flask or cloud API Gateway).
Usage:
python3 generate_form.py # local mode
python3 generate_form.py --api-url URL --google-client-id ID # cloud mode
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
PROJECT_ROOT = Path(__file__).resolve().parents[2]
OUTPUT_DIR = PROJECT_ROOT / "output"
CONFIG_PATH = PROJECT_ROOT / "config.json"
TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
def load_config():
with open(CONFIG_PATH) as f:
return json.load(f)
def latest_menu() -> dict:
files = sorted(OUTPUT_DIR.glob("menu-*.json"), reverse=True)
if not files:
print("Error: No menu JSON found. Run scrape_menu.py first.", file=sys.stderr)
sys.exit(1)
with open(files[0]) as f:
return json.load(f)
def _json_for_script(value) -> str:
"""Serialize for embedding in a <script> tag; escape < and JS line separators."""
return (
json.dumps(value, separators=(",", ":"))
.replace("<", "\\u003c")
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")
)
def generate_form(
menu: dict,
config: dict,
api_url: str = "",
bulk_discount: float = 0,
company_subsidy: float = 0,
google_client_id: str = "",
) -> str:
google_client_id = google_client_id.strip()
if api_url and not google_client_id:
raise ValueError("Google client ID is required in cloud mode")
week = datetime.now().strftime("%Y-W%U")
scraped_at = menu.get("scraped_at", "unknown")
deadline = config.get("order_deadline", "Thursday 11:59 PM")
has_discount = bulk_discount > 0 or company_subsidy > 0
use_google_auth = bool(google_client_id)
auth_mode = "google" if use_google_auth else "local"
form_config = {
"authMode": auth_mode,
"meals": menu["meals"],
"roster": config.get("roster", []),
"week": week,
"submitUrl": f"{api_url}/api/submit-order" if api_url else "/api/submit-order",
"statusUrl": (
f"{api_url}/api/form-status/{week}"
if api_url
else f"/api/form-status/{week}"
),
"rosterUrl": f"{api_url}/api/roster" if api_url else "/api/roster",
"adminUrl": f"{api_url}/api/admin/orders" if api_url else "/api/admin/orders",
"adminPdfUrl": (
f"{api_url}/api/admin/summary-pdf" if api_url else "/api/admin/summary-pdf"
),
"bulkDiscount": bulk_discount,
"companySubsidy": company_subsidy,
"googleClientId": google_client_id if use_google_auth else "",
"deadline": deadline,
"scrapedAt": scraped_at,
"hasDiscount": has_discount,
}
env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=select_autoescape(["html", "xml", "j2"]),
)
template = env.get_template("form.html.j2")
return template.render(
config=form_config,
config_json=_json_for_script(form_config),
css=(TEMPLATES_DIR / "form.css").read_text(encoding="utf-8"),
form_js=(TEMPLATES_DIR / "form.js").read_text(encoding="utf-8"),
admin_js=(TEMPLATES_DIR / "admin.js").read_text(encoding="utf-8"),
week=week,
scraped_at=scraped_at,
deadline=deadline,
has_discount=has_discount,
bulk_discount=bulk_discount,
company_subsidy=company_subsidy,
bulk_discount_label=f"{bulk_discount:g}",
company_subsidy_label=f"{company_subsidy:g}",
)
def main():
parser = argparse.ArgumentParser(description="Generate meal order form HTML")
parser.add_argument(
"--api-url", default="", help="API Gateway base URL (cloud mode)"
)
parser.add_argument(
"--bulk-discount",
type=float,
default=None,
help="Bulk discount percentage (e.g., 10 for 10%% off)",
)
parser.add_argument(
"--company-subsidy",
type=float,
default=None,
help="Company subsidy percentage (e.g., 50 for 50%% off after bulk discount)",
)
parser.add_argument(
"--google-client-id",
default=None,
help="Google OAuth Client ID for Sign-In authentication",
)
args = parser.parse_args()
config = load_config()
menu = latest_menu()
bulk_discount = (
args.bulk_discount
if args.bulk_discount is not None
else config.get("bulk_discount_percent", 0)
)
company_subsidy = (
args.company_subsidy
if args.company_subsidy is not None
else config.get("company_subsidy_percent", 0)
)
google_client_id = (
args.google_client_id or config.get("google_client_id", "")
).strip()
if args.api_url and not google_client_id:
try:
import boto3
ssm = boto3.client("ssm")
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
google_client_id = resp["Parameter"]["Value"].strip()
except Exception as exc:
parser.error(
"Google auth is required in cloud mode, but the Google client ID "
f"could not be loaded from SSM: {exc}"
)
if args.api_url and not google_client_id:
parser.error("Google auth is required in cloud mode")
html = generate_form(
menu,
config,
api_url=args.api_url,
bulk_discount=bulk_discount,
company_subsidy=company_subsidy,
google_client_id=google_client_id,
)
week = datetime.now().strftime("%Y-W%U")
output_file = OUTPUT_DIR / f"order-form-{week}.html"
with open(output_file, "w") as f:
f.write(html)
mode = "cloud" if args.api_url else "local"
print(f"Generated order form ({mode} mode): {output_file}")
print(f" {menu['meal_count']} meals from menu scraped {menu['scraped_at'][:10]}")
if mode == "local":
print(" Start the server with: python3 src/server/app.py")
else:
print(f" API endpoint: {args.api_url}")
if __name__ == "__main__":
main()