src/auth/routes.py : routes de réinitialisation du mot de passe (#73)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -92,3 +92,56 @@ def login():
|
|||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect("/")
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/request-password-reset", methods=["POST"])
|
||||||
|
def request_password_reset():
|
||||||
|
email = (request.form.get("email") or "").strip().lower()
|
||||||
|
try:
|
||||||
|
valid = validate_email(email, check_deliverability=False)
|
||||||
|
email = valid.normalized.lower()
|
||||||
|
except EmailNotValidError:
|
||||||
|
return redirect("/mot-de-passe-oublie?pending=1")
|
||||||
|
|
||||||
|
row = db.get_user_by_email(email)
|
||||||
|
if row is None:
|
||||||
|
return redirect("/mot-de-passe-oublie?pending=1")
|
||||||
|
|
||||||
|
token = tokens.create_password_reset_token(row["id"])
|
||||||
|
try:
|
||||||
|
mailer.send_reset_email(email, token)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Échec d'envoi de l'email de réinitialisation")
|
||||||
|
return _redirect_with_error("/mot-de-passe-oublie", "email_send_failed", email)
|
||||||
|
return redirect("/mot-de-passe-oublie?pending=1")
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/reset-password", methods=["POST"])
|
||||||
|
def reset_password():
|
||||||
|
token = request.form.get("token") or ""
|
||||||
|
password = request.form.get("password") or ""
|
||||||
|
password_confirm = request.form.get("password_confirm") or ""
|
||||||
|
|
||||||
|
user_id = tokens.validate_password_reset_token(token)
|
||||||
|
if user_id is None:
|
||||||
|
return redirect(
|
||||||
|
f"/reinitialiser-mot-de-passe?token={token}&error=invalid_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(password) < MIN_PASSWORD_LENGTH:
|
||||||
|
return redirect(
|
||||||
|
f"/reinitialiser-mot-de-passe?token={token}&error=password_too_short"
|
||||||
|
)
|
||||||
|
if password != password_confirm:
|
||||||
|
return redirect(
|
||||||
|
f"/reinitialiser-mot-de-passe?token={token}&error=password_mismatch"
|
||||||
|
)
|
||||||
|
|
||||||
|
consumed = tokens.consume_password_reset_token(token)
|
||||||
|
if consumed is None:
|
||||||
|
return redirect(
|
||||||
|
f"/reinitialiser-mot-de-passe?token={token}&error=invalid_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
db.update_password_hash(consumed, generate_password_hash(password))
|
||||||
|
return redirect("/connexion?password_changed=1")
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
|
|
||||||
|
from src.auth import db, tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _make_user():
|
||||||
|
db.init_schema()
|
||||||
|
uid = db.create_user("a@b.c", generate_password_hash("old-password12"))
|
||||||
|
db.set_email_verified(uid)
|
||||||
|
return uid
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_reset_sends_email_for_existing_user(
|
||||||
|
client, mail_outbox, users_db_path
|
||||||
|
):
|
||||||
|
_make_user()
|
||||||
|
resp = client.post("/auth/request-password-reset", data={"email": "a@b.c"})
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "pending=1" in resp.headers["Location"]
|
||||||
|
assert len(mail_outbox) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_reset_same_response_for_unknown_email(
|
||||||
|
client, mail_outbox, users_db_path
|
||||||
|
):
|
||||||
|
db.init_schema()
|
||||||
|
resp = client.post("/auth/request-password-reset", data={"email": "absent@b.c"})
|
||||||
|
assert "pending=1" in resp.headers["Location"]
|
||||||
|
assert len(mail_outbox) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_reset_with_valid_token(client, users_db_path):
|
||||||
|
uid = _make_user()
|
||||||
|
token = tokens.create_password_reset_token(uid)
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/reset-password",
|
||||||
|
data={
|
||||||
|
"token": token,
|
||||||
|
"password": "new-password12",
|
||||||
|
"password_confirm": "new-password12",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 302
|
||||||
|
assert "password_changed=1" in resp.headers["Location"]
|
||||||
|
row = db.get_user_by_id(uid)
|
||||||
|
assert check_password_hash(row["password_hash"], "new-password12")
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_reset_rejects_expired_token(client, users_db_path):
|
||||||
|
uid = _make_user()
|
||||||
|
token = tokens.create_password_reset_token(uid, expires_in_hours=-1)
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/reset-password",
|
||||||
|
data={
|
||||||
|
"token": token,
|
||||||
|
"password": "new-password12",
|
||||||
|
"password_confirm": "new-password12",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "error=invalid_token" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_reset_rejects_short_password(client, users_db_path):
|
||||||
|
uid = _make_user()
|
||||||
|
token = tokens.create_password_reset_token(uid)
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/reset-password",
|
||||||
|
data={"token": token, "password": "short", "password_confirm": "short"},
|
||||||
|
)
|
||||||
|
assert "error=password_too_short" in resp.headers["Location"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_perform_reset_rejects_mismatched(client, users_db_path):
|
||||||
|
uid = _make_user()
|
||||||
|
token = tokens.create_password_reset_token(uid)
|
||||||
|
resp = client.post(
|
||||||
|
"/auth/reset-password",
|
||||||
|
data={
|
||||||
|
"token": token,
|
||||||
|
"password": "new-password12",
|
||||||
|
"password_confirm": "other-password99",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert "error=password_mismatch" in resp.headers["Location"]
|
||||||
Reference in New Issue
Block a user