diff --git a/boker/app.py b/boker/app.py index e99ad0c..c307562 100644 --- a/boker/app.py +++ b/boker/app.py @@ -3,9 +3,11 @@ from __future__ import annotations import click import os +from urllib.parse import urlsplit from uuid import uuid4 -from flask import Flask, render_template, request +from flask import Flask, flash, redirect, render_template, request, url_for +from flask_wtf.csrf import CSRFError from werkzeug.exceptions import HTTPException from boker.auth import current_user_id, is_logged_in, is_site_admin, normalize_email @@ -82,6 +84,21 @@ def create_app(config_overrides: dict | None = None) -> Flask: def forbidden(e): return render_template("403.html"), 403 + @app.errorhandler(CSRFError) + def csrf_error(error): + flash("Your form expired. Please try again.", "error") + + referrer = request.referrer or "" + parsed = urlsplit(referrer) + if parsed.netloc == request.host and parsed.path: + return redirect(parsed.path) + + if request.path.startswith("/account/login"): + return redirect(url_for("account.login")) + if request.path.startswith("/account/register"): + return redirect(url_for("account.register")) + return redirect(url_for("public.home")) + @app.errorhandler(Exception) def unexpected_error(error): if isinstance(error, HTTPException): diff --git a/boker/config.py b/boker/config.py index 2077e0f..151384e 100644 --- a/boker/config.py +++ b/boker/config.py @@ -8,7 +8,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" DEFAULT_SECRET_KEY = "change-this-before-deploying" -APP_VERSION = "2.5.32" +APP_VERSION = "2.5.33" def load_local_env(env_path: Path) -> None: @@ -59,5 +59,5 @@ class Config: class ProductionConfig(Config): SESSION_COOKIE_SECURE: bool = True - SESSION_COOKIE_SAMESITE: str = "Strict" + SESSION_COOKIE_SAMESITE: str = "Lax" PREFERRED_URL_SCHEME: str = "https" diff --git a/tests/test_config.py b/tests/test_config.py index 962a001..310bab7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -35,7 +35,7 @@ class ProductionConfigTests(unittest.TestCase): ) self.assertTrue(app.config["SESSION_COOKIE_SECURE"]) - self.assertEqual(app.config["SESSION_COOKIE_SAMESITE"], "Strict") + self.assertEqual(app.config["SESSION_COOKIE_SAMESITE"], "Lax") self.assertTrue(app.config["SQLALCHEMY_DATABASE_URI"].startswith("postgresql+psycopg://")) diff --git a/tests/test_errors.py b/tests/test_errors.py index ea0d844..02c610e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -42,6 +42,28 @@ class ErrorPageTests(unittest.TestCase): self.assertNotIn(b"RuntimeError", response.data) self.assertRegex(response.get_data(as_text=True), re.compile(r"Incident [a-f0-9]{12}")) + def test_csrf_errors_redirect_to_form_with_flash(self): + csrf_app = create_app( + { + "TESTING": True, + "PROPAGATE_EXCEPTIONS": False, + "SQLALCHEMY_DATABASE_URI": self.app.config["SQLALCHEMY_DATABASE_URI"], + "WTF_CSRF_ENABLED": True, + } + ) + client = csrf_app.test_client() + + response = client.post( + "/account/login", + data={"csrf_token": "stale-token", "email": "owner@example.com", "password": "password123"}, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.headers["Location"], "/account/login") + with client.session_transaction() as flask_session: + flashes = flask_session.get("_flashes", []) + self.assertIn(("error", "Your form expired. Please try again."), flashes) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_seo.py b/tests/test_seo.py index 3c87ad6..221296b 100644 --- a/tests/test_seo.py +++ b/tests/test_seo.py @@ -34,9 +34,11 @@ class SeoTests(unittest.TestCase): html = response.get_data(as_text=True) self.assertEqual(response.status_code, 200) - self.assertIn("Free Home Poker Tracker, Ledger & League Leaderboards", html) + self.assertIn("Free Poker Tracker, Ledger, Stats & Profit/Loss App", html) self.assertIn('name="description"', html) - self.assertIn("Track home poker sessions, buy-ins, cashouts, settlements", html) + self.assertIn("free poker tracker and poker ledger for home games", html) + self.assertIn("My Boker", html) + self.assertIn("poker profit or loss", html) self.assertIn('rel="canonical" href="https://myboker.org/"', html) self.assertIn('application/ld+json', html)