resolve mobile login

This commit is contained in:
SowinskiBraeden committed 2026-06-29 20:32:10 -07:00
1 parent 6b4b283679
commit c49c84ad4e
4 files changed
+83 -6

No files matched your search

+41 -3
View File
@@ -5,7 +5,8 @@ import secrets
from datetime import datetime, timedelta, timezone
from urllib.parse import urlsplit
from flask import Blueprint, flash, redirect, render_template, request, session as flask_session, url_for
from flask import Blueprint, current_app, flash, redirect, render_template, request, session as flask_session, url_for
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from boker.auth import (
current_user_id,
@@ -21,10 +22,11 @@ from boker.auth import (
verify_reset_token,
)
from boker.db import database_extensions_available, db
from boker.extensions import limiter
from boker.extensions import csrf, limiter
account_bp = Blueprint("account", __name__, url_prefix="/account")
EMAIL_VERIFICATION_TTL = timedelta(minutes=15)
LOGIN_CSRF_TTL_SECONDS = 3600
def db_ready() -> bool:
@@ -63,6 +65,23 @@ def _new_verification_code() -> str:
return f"{secrets.randbelow(1000000):06d}"
def _login_csrf_serializer() -> URLSafeTimedSerializer:
return URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
def _issue_login_csrf_token() -> str:
return _login_csrf_serializer().dumps("login", salt="login-form")
def _valid_login_csrf_token(token: str) -> bool:
if not token:
return False
try:
return _login_csrf_serializer().loads(token, salt="login-form", max_age=LOGIN_CSRF_TTL_SECONDS) == "login"
except (BadSignature, SignatureExpired):
return False
def _issue_verification_code(user) -> str:
from boker.db_models import utc_now
@@ -144,6 +163,7 @@ def register():
@account_bp.route("/login", methods=["GET", "POST"])
@csrf.exempt
@limiter.limit("20 per minute")
def login():
if current_user_id():
@@ -156,10 +176,23 @@ def login():
form = {
"email": request.form.get("email", "").strip(),
}
login_csrf_error = None
if request.method == "POST":
from boker.league_repositories import find_user_by_email
if not _valid_login_csrf_token(request.form.get("login_csrf_token", "")):
login_csrf_error = "Your sign-in form expired. Please try again."
return (
render_template(
"account_login.html",
form=form,
login_csrf_token=_issue_login_csrf_token(),
login_csrf_error=login_csrf_error,
),
400,
)
user = find_user_by_email(form["email"])
password = request.form.get("password", "")
@@ -182,7 +215,12 @@ def login():
flash("Logged in.", "success")
return redirect(next_url)
return render_template("account_login.html", form=form)
return render_template(
"account_login.html",
form=form,
login_csrf_token=_issue_login_csrf_token(),
login_csrf_error=login_csrf_error,
)
@account_bp.route("/verify-email", methods=["GET", "POST"])
+11
View File
@@ -5813,6 +5813,17 @@ select.control { cursor: pointer; }
line-height: 1.58;
}
.auth-inline-error {
margin: 0 0 18px;
padding: 11px 14px;
border-radius: var(--r-sm);
border: 1px solid var(--neg-tint-bd);
background: var(--neg-tint);
color: var(--neg);
font-size: 14px;
line-height: 1.45;
}
.auth-field {
display: grid;
gap: 7px;
+4 -1
View File
@@ -13,9 +13,12 @@
<h1 class="auth-split__title">Welcome back.</h1>
<p class="auth-split__sub">Sign in to manage your leagues, sessions, and ledger.</p>
{% if login_csrf_error %}
<div class="auth-inline-error" role="alert">{{ login_csrf_error }}</div>
{% endif %}
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="login_csrf_token" value="{{ login_csrf_token }}">
<div class="auth-field">
<label for="email">Email</label>
<input id="email" type="email" name="email" autocomplete="email"
+27 -2
View File
@@ -54,16 +54,41 @@ class ErrorPageTests(unittest.TestCase):
client = csrf_app.test_client()
response = client.post(
"/account/login",
"/account/register",
data={"csrf_token": "stale-token", "email": "owner@example.com", "password": "password123"},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.headers["Location"], "/account/login")
self.assertEqual(response.headers["Location"], "/account/register")
with client.session_transaction() as flask_session:
flashes = flask_session.get("_flashes", [])
self.assertIn(("error", "Your form expired. Please try again."), flashes)
def test_login_uses_stateless_csrf_error(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()
login_page = client.get("/account/login")
self.assertEqual(login_page.status_code, 200)
self.assertIsNone(login_page.headers.get("Set-Cookie"))
self.assertIn(b'name="login_csrf_token"', login_page.data)
response = client.post(
"/account/login",
data={"email": "owner@example.com", "password": "password123"},
)
self.assertEqual(response.status_code, 400)
self.assertIn(b"Your sign-in form expired. Please try again.", response.data)
self.assertIn(b'name="login_csrf_token"', response.data)
if __name__ == "__main__":
unittest.main()