"""Flask entrypoint.
App Engine runs this as `gunicorn -c gunicorn.py main:app`, so `app` must exist
at module scope.
"""
import logging
from datetime import timedelta
from flask import Flask, render_template
import settings
from app import routes
from utils.db_wrapper import db_session
logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# The session cookie is a credential: it is what says a request is signed in.
# Signing it with the shipped development key would let anyone forge one, so a
# deployed environment refuses to start rather than running with a known secret.
# Empty is included because a deploy that forgets to render SECRET_KEY leaves it "".
_WEAK_SECRETS = {"", "dev-only-not-for-production", "changeme", "secret"}
if settings.Config.ENV == "prod" and settings.Config.SECRET_KEY.strip() in _WEAK_SECRETS:
raise RuntimeError(
"SECRET_KEY is unset or still a placeholder. Set a real one before "
"running in prod - it signs the session cookie, and the cookie is what "
"says a request is authenticated."
)
app.config["SECRET_KEY"] = settings.Config.SECRET_KEY
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
SESSION_COOKIE_SECURE=settings.Config.ENV == "prod",
PERMANENT_SESSION_LIFETIME=timedelta(days=14),
)
routes.init_app(app)
@app.context_processor
def inject_template_globals():
"""`csrf_token` renders into a meta tag on every page; app.js reads it and
echoes it on state-changing requests (see endpoints/crud.check_csrf).
`google_enabled` lets the sign-in page show the Google button only when it is
configured - the same "unset config = feature hidden" stance the rest of the
app takes. Both are passed as functions, so a template calls them."""
from app.endpoints.crud import csrf_token
from app.endpoints.oauth import is_enabled as google_enabled
return {"csrf_token": csrf_token, "google_enabled": google_enabled}
@app.teardown_appcontext
def shutdown_session(exception=None):
"""Return the request's connection to the pool. Without this, scoped_session
leaks a connection per request until the pool is exhausted."""
db_session.remove()
@app.route("/")
def home():
# Reached signed-in only: "/" is not in PUBLIC_PATHS, so require_session has
# already redirected a signed-out visitor to /login. If this app should have
# a public landing page, add "/" to that allowlist and decide here what a
# stranger sees.
return render_template("home.html")
# App Engine's frontend swallows /healthz before it reaches the app, so /health
# is the alias that actually works there; both are kept so the same app is
# probeable locally and anywhere else.
@app.route("/health")
@app.route("/healthz")
def healthz():
return {"status": "ok"}
if __name__ == "__main__":
app.run(host="127.0.0.1", port=8080, debug=True)