Prepare redesign for public deployment
This commit is contained in:
7 files changed
+27
-7
No files matched your search
@@ -1,6 +1,8 @@
|
|||||||
SECRET_KEY=replace-this-with-a-long-random-string
|
SECRET_KEY=replace-this-with-a-long-random-string
|
||||||
|
APP_ENV=production
|
||||||
DATABASE_URL=postgresql+psycopg://user:password@host/dbname
|
DATABASE_URL=postgresql+psycopg://user:password@host/dbname
|
||||||
APP_BASE_URL=https://myboker.org
|
APP_BASE_URL=https://myboker.org
|
||||||
|
RATELIMIT_STORAGE_URI=redis://localhost:6379/0
|
||||||
MAIL_SERVER=smtp.sendgrid.net
|
MAIL_SERVER=smtp.sendgrid.net
|
||||||
MAIL_PORT=587
|
MAIL_PORT=587
|
||||||
MAIL_USE_TLS=true
|
MAIL_USE_TLS=true
|
||||||
|
|||||||
@@ -74,8 +74,10 @@ Then open:
|
|||||||
The app reads these from `.env`:
|
The app reads these from `.env`:
|
||||||
|
|
||||||
- `SECRET_KEY`
|
- `SECRET_KEY`
|
||||||
|
- `APP_ENV`
|
||||||
- `DATABASE_URL`
|
- `DATABASE_URL`
|
||||||
- `APP_BASE_URL`
|
- `APP_BASE_URL`
|
||||||
|
- `RATELIMIT_STORAGE_URI`
|
||||||
- `MAIL_SERVER`
|
- `MAIL_SERVER`
|
||||||
- `MAIL_PORT`
|
- `MAIL_PORT`
|
||||||
- `MAIL_USE_TLS`
|
- `MAIL_USE_TLS`
|
||||||
@@ -84,6 +86,8 @@ The app reads these from `.env`:
|
|||||||
- `MAIL_PASSWORD`
|
- `MAIL_PASSWORD`
|
||||||
- `MAIL_DEFAULT_SENDER`
|
- `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.
|
||||||
|
|
||||||
## Site Admin Access
|
## Site Admin Access
|
||||||
|
|
||||||
Internal admin access uses normal database-backed accounts, not hardcoded `.env` credentials.
|
Internal admin access uses normal database-backed accounts, not hardcoded `.env` credentials.
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import os
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
|
||||||
from auth import current_user_id, is_logged_in, is_site_admin, normalize_email
|
from auth import current_user_id, is_logged_in, is_site_admin, normalize_email
|
||||||
from config import Config
|
from config import DEFAULT_SECRET_KEY, Config, ProductionConfig
|
||||||
from db import database_extensions_available, db, init_database
|
from db import database_extensions_available, db, init_database
|
||||||
from extensions import csrf, limiter, mail
|
from extensions import csrf, limiter, mail
|
||||||
from routes.account import account_bp
|
from routes.account import account_bp
|
||||||
@@ -16,12 +17,22 @@ from storage import ensure_data_file
|
|||||||
from utils import cents_to_dollars, safe_date_label
|
from utils import cents_to_dollars, safe_date_label
|
||||||
|
|
||||||
|
|
||||||
|
def _config_for_environment():
|
||||||
|
app_env = os.getenv("APP_ENV", os.getenv("FLASK_ENV", "")).lower()
|
||||||
|
if app_env in {"prod", "production"}:
|
||||||
|
return ProductionConfig
|
||||||
|
return Config
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_overrides: dict | None = None) -> Flask:
|
def create_app(config_overrides: dict | None = None) -> Flask:
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(_config_for_environment())
|
||||||
if config_overrides:
|
if config_overrides:
|
||||||
app.config.update(config_overrides)
|
app.config.update(config_overrides)
|
||||||
|
|
||||||
|
if app.config["SESSION_COOKIE_SECURE"] and app.config["SECRET_KEY"] == DEFAULT_SECRET_KEY:
|
||||||
|
raise RuntimeError("Set SECRET_KEY before running in production.")
|
||||||
|
|
||||||
ensure_data_file(app.config["DATA_PATH"])
|
ensure_data_file(app.config["DATA_PATH"])
|
||||||
init_database(app)
|
init_database(app)
|
||||||
csrf.init_app(app)
|
csrf.init_app(app)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
BASE_DIR = Path(__file__).resolve().parent
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
DATA_PATH = BASE_DIR / "data" / "entries.csv"
|
DATA_PATH = BASE_DIR / "data" / "entries.csv"
|
||||||
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}"
|
||||||
|
DEFAULT_SECRET_KEY = "change-this-before-deploying"
|
||||||
|
|
||||||
ELIGIBLE_MIN_SESSIONS = 3
|
ELIGIBLE_MIN_SESSIONS = 3
|
||||||
APP_VERSION = "2.5.29"
|
APP_VERSION = "2.5.29"
|
||||||
@@ -32,7 +33,7 @@ load_local_env(BASE_DIR / ".env")
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
APP_VERSION: str = APP_VERSION
|
APP_VERSION: str = APP_VERSION
|
||||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "change-this-before-deploying")
|
SECRET_KEY: str = os.getenv("SECRET_KEY", DEFAULT_SECRET_KEY)
|
||||||
SQLALCHEMY_DATABASE_URI: str = os.getenv("DATABASE_URL", DEFAULT_DATABASE_URL)
|
SQLALCHEMY_DATABASE_URI: str = os.getenv("DATABASE_URL", DEFAULT_DATABASE_URL)
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
|
SQLALCHEMY_TRACK_MODIFICATIONS: bool = False
|
||||||
SESSION_COOKIE_HTTPONLY: bool = True
|
SESSION_COOKIE_HTTPONLY: bool = True
|
||||||
@@ -52,6 +53,7 @@ class Config:
|
|||||||
MAIL_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None
|
MAIL_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None
|
||||||
MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org")
|
MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org")
|
||||||
MAIL_TIMEOUT: float = float(os.getenv("MAIL_TIMEOUT", "5"))
|
MAIL_TIMEOUT: float = float(os.getenv("MAIL_TIMEOUT", "5"))
|
||||||
|
RATELIMIT_STORAGE_URI: str | None = os.getenv("RATELIMIT_STORAGE_URI") or None
|
||||||
|
|
||||||
|
|
||||||
class ProductionConfig(Config):
|
class ProductionConfig(Config):
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ def downgrade() -> None:
|
|||||||
op.drop_index(op.f("ix_league_memberships_league_id"), table_name="league_memberships")
|
op.drop_index(op.f("ix_league_memberships_league_id"), table_name="league_memberships")
|
||||||
op.drop_table("league_memberships")
|
op.drop_table("league_memberships")
|
||||||
op.drop_index(op.f("ix_leagues_slug"), table_name="leagues")
|
op.drop_index(op.f("ix_leagues_slug"), table_name="leagues")
|
||||||
op.drop_index(op.f("ix_leagues_public_key"), table_name="leagues")
|
|
||||||
op.drop_index(op.f("ix_leagues_created_by_user_id"), table_name="leagues")
|
op.drop_index(op.f("ix_leagues_created_by_user_id"), table_name="leagues")
|
||||||
op.drop_table("leagues")
|
op.drop_table("leagues")
|
||||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ Flask-Mail>=0.10,<1.0
|
|||||||
Flask-Migrate>=4.0,<5.0
|
Flask-Migrate>=4.0,<5.0
|
||||||
Flask-SQLAlchemy>=3.1,<4.0
|
Flask-SQLAlchemy>=3.1,<4.0
|
||||||
Flask-WTF>=1.2,<2.0
|
Flask-WTF>=1.2,<2.0
|
||||||
|
gunicorn>=22,<24
|
||||||
psycopg[binary]>=3.2,<4.0
|
psycopg[binary]>=3.2,<4.0
|
||||||
|
redis>=5,<6
|
||||||
@@ -103,9 +103,9 @@ class InternalAdminAccessTests(unittest.TestCase):
|
|||||||
self.assertIn(b"Overview", response.data)
|
self.assertIn(b"Overview", response.data)
|
||||||
self.assertIn(b"admin@example.com", response.data)
|
self.assertIn(b"admin@example.com", response.data)
|
||||||
self.assertIn(b"Active users", response.data)
|
self.assertIn(b"Active users", response.data)
|
||||||
self.assertIn(b"Returning users", response.data)
|
self.assertIn(b"Activity breakdown", response.data)
|
||||||
self.assertIn(b"Recorded ledger volume", response.data)
|
self.assertIn(b"Ledger events", response.data)
|
||||||
self.assertIn(b"Not platform revenue", response.data)
|
self.assertIn(b"Totals at a glance", response.data)
|
||||||
|
|
||||||
def test_admin_can_search_users(self):
|
def test_admin_can_search_users(self):
|
||||||
self.login_as(self.admin_id)
|
self.login_as(self.admin_id)
|
||||||
|
|||||||
Reference in new issue
Block a user