src/auth/routes.py : routes /auth/login et /auth/logout (#73)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+34
-1
@@ -1,14 +1,20 @@
|
|||||||
from email_validator import EmailNotValidError, validate_email
|
from email_validator import EmailNotValidError, validate_email
|
||||||
from flask import Blueprint, redirect, request
|
from flask import Blueprint, redirect, request
|
||||||
from werkzeug.security import generate_password_hash
|
from flask_login import login_user, logout_user
|
||||||
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
from src.auth import db, mailer, tokens
|
from src.auth import db, mailer, tokens
|
||||||
|
from src.auth.models import User
|
||||||
|
from src.auth.setup import safe_next
|
||||||
from src.utils import logger
|
from src.utils import logger
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
|
||||||
|
|
||||||
MIN_PASSWORD_LENGTH = 8
|
MIN_PASSWORD_LENGTH = 8
|
||||||
|
|
||||||
|
# Hash bidon pré-calculé pour uniformiser le timing login
|
||||||
|
_DUMMY_HASH = generate_password_hash("dummy-password-for-timing")
|
||||||
|
|
||||||
|
|
||||||
def _redirect_with_error(path: str, error: str, email: str | None = None):
|
def _redirect_with_error(path: str, error: str, email: str | None = None):
|
||||||
url = f"{path}?error={error}"
|
url = f"{path}?error={error}"
|
||||||
@@ -59,3 +65,30 @@ def verify_email():
|
|||||||
return redirect("/verification-email?error=invalid_token")
|
return redirect("/verification-email?error=invalid_token")
|
||||||
db.set_email_verified(user_id)
|
db.set_email_verified(user_id)
|
||||||
return redirect("/connexion?verified=1")
|
return redirect("/connexion?verified=1")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/login", methods=["POST"])
|
||||||
|
def login():
|
||||||
|
email = (request.form.get("email") or "").strip().lower()
|
||||||
|
password = request.form.get("password") or ""
|
||||||
|
next_url = safe_next(request.form.get("next"), fallback="/compte")
|
||||||
|
|
||||||
|
row = db.get_user_by_email(email)
|
||||||
|
if row is None:
|
||||||
|
check_password_hash(_DUMMY_HASH, password) # uniformiser le temps
|
||||||
|
return _redirect_with_error("/connexion", "invalid_credentials", email)
|
||||||
|
|
||||||
|
if not check_password_hash(row["password_hash"], password):
|
||||||
|
return _redirect_with_error("/connexion", "invalid_credentials", email)
|
||||||
|
|
||||||
|
if not row["email_verified"]:
|
||||||
|
return _redirect_with_error("/connexion", "email_not_verified", email)
|
||||||
|
|
||||||
|
login_user(User(row), remember=True)
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/logout", methods=["POST"])
|
||||||
|
def logout():
|
||||||
|
logout_user()
|
||||||
|
return redirect("/")
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
|
from src.auth import db
|
||||||
|
|
||||||
|
|
||||||
|
def _make_verified_user(email="a@b.c", password="password12"):
|
||||||
|
db.init_schema()
|
||||||
|
uid = db.create_user(email, generate_password_hash(password))
|
||||||
|
db.set_email_verified(uid)
|
||||||
|
return uid
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_success(client, users_db_path):
|
||||||
|
_make_verified_user()
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
data={"email": "a@b.c", "password": "password12"},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert resp.headers["Location"].endswith("/compte")
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_wrong_password(client, users_db_path):
|
||||||
|
_make_verified_user()
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/login", data={"email": "a@b.c", "password": "wrong-password"}
|
||||||
|
)
|
||||||
|
assert "error=invalid_credentials" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_unknown_email_same_error(client, users_db_path):
|
||||||
|
db.init_schema()
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
data={"email": "inexistant@example.com", "password": "x" * 12},
|
||||||
|
)
|
||||||
|
assert "error=invalid_credentials" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_unverified_user(client, users_db_path):
|
||||||
|
db.init_schema()
|
||||||
|
db.create_user("a@b.c", generate_password_hash("password12"))
|
||||||
|
resp = client.post("/auth/login", data={"email": "a@b.c", "password": "password12"})
|
||||||
|
assert "error=email_not_verified" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_respects_safe_next(client, users_db_path):
|
||||||
|
_make_verified_user()
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
data={"email": "a@b.c", "password": "password12", "next": "/tableau"},
|
||||||
|
)
|
||||||
|
assert resp.headers["Location"].endswith("/tableau")
|
||||||
|
|
||||||
|
|
||||||
|
def test_login_rejects_absolute_next(client, users_db_path):
|
||||||
|
_make_verified_user()
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
data={
|
||||||
|
"email": "a@b.c",
|
||||||
|
"password": "password12",
|
||||||
|
"next": "https://evil.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.headers["Location"].endswith("/compte")
|
||||||
|
|
||||||
|
|
||||||
|
def test_logout_clears_session(client, users_db_path):
|
||||||
|
_make_verified_user()
|
||||||
|
client.post("/auth/login", data={"email": "a@b.c", "password": "password12"})
|
||||||
|
resp = client.post("/auth/logout")
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert resp.headers["Location"].endswith("/")
|
||||||
Reference in New Issue
Block a user