From 558df9627beb9f905318968ddf8e36ab38d32eab Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 24 Jun 2026 18:48:08 +0200 Subject: [PATCH] feat(auth): routes change-email + confirm-email-change (#73) Co-Authored-By: Claude Sonnet 4.6 --- src/auth/routes.py | 34 +++++++++++++++++++++ tests/auth/test_email_change.py | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/auth/test_email_change.py diff --git a/src/auth/routes.py b/src/auth/routes.py index e763df8..c50e7cf 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -165,3 +165,37 @@ def change_password(): db.update_password_hash(current_user.id, generate_password_hash(password)) return redirect("/compte?password_changed=1") + + +@auth_bp.route("/change-email", methods=["POST"]) +@login_required +def change_email(): + email = (request.form.get("email") or "").strip() + try: + valid = validate_email(email, check_deliverability=False) + email = valid.normalized.lower() + except EmailNotValidError: + return _redirect_with_error("/compte/admin", "invalid_email") + + if db.get_user_by_email(email) is not None: + return _redirect_with_error("/compte/admin", "email_taken") + + db.set_pending_email(current_user.id, email) + token = tokens.create_verification_token(current_user.id) + try: + mailer.send_email_change_email(email, token) + except Exception: + logger.exception("Échec d'envoi de l'email de changement d'adresse") + return _redirect_with_error("/compte/admin", "email_send_failed") + + return redirect("/compte/admin?email_pending=1") + + +@auth_bp.route("/confirm-email-change", methods=["GET"]) +def confirm_email_change(): + token = request.args.get("token") or "" + user_id = tokens.consume_verification_token(token) + if user_id is None: + return redirect("/compte/admin?error=invalid_token") + db.promote_pending_email(user_id) + return redirect("/compte/admin?email_changed=1") diff --git a/tests/auth/test_email_change.py b/tests/auth/test_email_change.py new file mode 100644 index 0000000..cb3db43 --- /dev/null +++ b/tests/auth/test_email_change.py @@ -0,0 +1,53 @@ +from werkzeug.security import generate_password_hash + +from src.auth import db + + +def _login(client, email="old@example.fr", password="password12"): + db.init_schema() + uid = db.create_user(email, generate_password_hash(password)) + db.set_email_verified(uid) + client.post("/auth/login", data={"email": email, "password": password}) + return uid + + +def test_change_email_requires_login(client, users_db_path): + resp = client.post("/auth/change-email", data={"email": "x@y.z"}) + assert resp.status_code in (302, 401) + + +def test_change_email_sets_pending_and_sends_mail(client, users_db_path, mail_outbox): + uid = _login(client) + resp = client.post("/auth/change-email", data={"email": "new@example.fr"}) + assert resp.status_code == 302 + assert "email_pending=1" in resp.headers["Location"] + assert db.get_user_by_id(uid)["pending_email"] == "new@example.fr" + assert db.get_user_by_id(uid)["email"] == "old@example.fr" # pas encore changé + assert mail_outbox[0].recipients == ["new@example.fr"] + + +def test_change_email_invalid(client, users_db_path, mail_outbox): + _login(client) + resp = client.post("/auth/change-email", data={"email": "pas-un-email"}) + assert "error=invalid_email" in resp.headers["Location"] + assert mail_outbox == [] + + +def test_change_email_already_taken(client, users_db_path, mail_outbox): + _login(client) + db.create_user("taken@example.fr", generate_password_hash("password12")) + resp = client.post("/auth/change-email", data={"email": "taken@example.fr"}) + assert "error=email_taken" in resp.headers["Location"] + assert mail_outbox == [] + + +def test_confirm_email_change_promotes(client, users_db_path, mail_outbox): + from src.auth import tokens + + uid = _login(client) + client.post("/auth/change-email", data={"email": "new@example.fr"}) + token = tokens.create_verification_token(uid) + resp = client.get(f"/auth/confirm-email-change?token={token}") + assert resp.status_code == 302 + assert "email_changed=1" in resp.headers["Location"] + assert db.get_user_by_id(uid)["email"] == "new@example.fr"