From 593420f984d5a7de4656de5f6ece7e780f613c5f Mon Sep 17 00:00:00 2001 From: SowinskiBraeden Date: Sun, 28 Jun 2026 17:52:22 -0700 Subject: [PATCH] Require production database configuration --- README.md | 2 +- boker/app.py | 6 +++++- tests/test_config.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/test_config.py diff --git a/README.md b/README.md index 361c621..c0f974b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The app reads these from `.env`: - `MAIL_PASSWORD` - `MAIL_DEFAULT_SENDER` -For public deployments, set `APP_ENV=production`. Production mode enables secure cookies and refuses to start with the development `SECRET_KEY`. Set `RATELIMIT_STORAGE_URI` to a shared backend such as Redis so login and signup limits are enforced across processes. +For public deployments, set `APP_ENV=production`. Production mode enables secure cookies and refuses to start with the development `SECRET_KEY` or the local SQLite database. Use PostgreSQL for `DATABASE_URL`, then run `flask --app app db upgrade` during deploy. Set `RATELIMIT_STORAGE_URI` to a shared backend such as Redis so login and signup limits are enforced across processes. ## Site Admin Access diff --git a/boker/app.py b/boker/app.py index 3465e12..bd8fdce 100644 --- a/boker/app.py +++ b/boker/app.py @@ -9,7 +9,7 @@ from flask import Flask, render_template, request from werkzeug.exceptions import HTTPException from boker.auth import current_user_id, is_logged_in, is_site_admin, normalize_email -from boker.config import DEFAULT_SECRET_KEY, Config, ProductionConfig +from boker.config import DEFAULT_DATABASE_URL, DEFAULT_SECRET_KEY, Config, ProductionConfig from boker.db import database_extensions_available, db, init_database from boker.extensions import csrf, limiter, mail from boker.routes.account import account_bp @@ -35,6 +35,10 @@ def create_app(config_overrides: dict | None = None) -> Flask: if app.config["SESSION_COOKIE_SECURE"] and app.config["SECRET_KEY"] == DEFAULT_SECRET_KEY: raise RuntimeError("Set SECRET_KEY before running in production.") + if app.config["SESSION_COOKIE_SECURE"]: + database_url = app.config["SQLALCHEMY_DATABASE_URI"] + if database_url == DEFAULT_DATABASE_URL or database_url.startswith("sqlite:"): + raise RuntimeError("Set DATABASE_URL to a production PostgreSQL database before running in production.") ensure_data_file(app.config["DATA_PATH"]) init_database(app) diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..d6c8027 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,37 @@ +import os +import unittest +from unittest.mock import patch + +from app import create_app + + +class ProductionConfigTests(unittest.TestCase): + def test_production_refuses_default_sqlite_database(self): + with patch.dict( + os.environ, + {"APP_ENV": "production"}, + clear=False, + ): + with self.assertRaisesRegex(RuntimeError, "DATABASE_URL"): + create_app({"SECRET_KEY": "test-production-secret"}) + + def test_production_accepts_postgresql_database_url(self): + with patch.dict( + os.environ, + {"APP_ENV": "production"}, + clear=False, + ): + app = create_app( + { + "SECRET_KEY": "test-production-secret", + "SQLALCHEMY_DATABASE_URI": "postgresql+psycopg://user:password@example.com/dbname", + } + ) + + self.assertTrue(app.config["SESSION_COOKIE_SECURE"]) + self.assertEqual(app.config["SESSION_COOKIE_SAMESITE"], "Strict") + self.assertTrue(app.config["SQLALCHEMY_DATABASE_URI"].startswith("postgresql+psycopg://")) + + +if __name__ == "__main__": + unittest.main()