diff --git a/app/routes/admin.py b/app/routes/admin.py index 98c688f..7ace6a2 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -354,7 +354,7 @@ def edit_user(user_id): new_pw = request.form.get('new_password', '') if new_pw: confirm_pw = request.form.get('confirm_password', '') - pw_error = validate_password(new_pw, confirm_pw) + pw_error = validate_password(new_pw, confirm_pw, user.password_hash) if pw_error: flash(pw_error, 'danger') return render_template('admin/edit_user.html', user=user, roles=_roles()) diff --git a/app/routes/auth.py b/app/routes/auth.py index ff6f689..6008855 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -173,7 +173,7 @@ def profile(): logger.info(f'[AUTH AVATAR UPLOAD] user_id={current_user.id} file={stored_name}') if new_pw: - pw_error = validate_password(new_pw, confirm_pw) + pw_error = validate_password(new_pw, confirm_pw, current_user.password_hash) if pw_error: flash(pw_error, 'danger') return render_template('auth/profile.html') @@ -236,12 +236,12 @@ def reset_password(token): if request.method == 'POST': password = request.form.get('password', '') confirm = request.form.get('confirm_password', '') - pw_error = validate_password(password, confirm) + user = token_row.user + pw_error = validate_password(password, confirm, user.password_hash) if pw_error: flash(pw_error, 'danger') return render_template('auth/reset_password.html', token=token) - user = token_row.user user.set_password(password) db.session.delete(token_row) # single-use — delete immediately log_action(user.id, 'password_reset', 'user', user.id) diff --git a/app/services/validation_service.py b/app/services/validation_service.py index 3f2ccec..cf36b17 100644 --- a/app/services/validation_service.py +++ b/app/services/validation_service.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) # ─── Password Validation ────────────────────────────────────────────────────── -def validate_password(password: str, confirm: str) -> str | None: +def validate_password(password: str, confirm: str, current_hash: str | None = None) -> str | None: """Validate a new password and its confirmation field. Returns an error message string if validation fails, or None if the @@ -27,11 +27,17 @@ def validate_password(password: str, confirm: str) -> str | None: - Must contain at least one uppercase letter (A-Z). - Must contain at least one digit (0-9). - Must contain at least one special character (!@#$%^&* etc.). + - If current_hash is given, the new password must differ from it. Parameters ---------- - password : str – the candidate password (plain text) - confirm : str – the confirmation field value + password : str – the candidate password (plain text) + confirm : str – the confirmation field value + current_hash : str|None – the user's existing password_hash, if this is + a password *change* (profile, admin edit, + reset) rather than new-account creation. + When given, re-using the current password is + rejected. """ import re if password != confirm: @@ -48,6 +54,12 @@ def validate_password(password: str, confirm: str) -> str | None: return 'Password must contain at least one number.' if not re.search(r'[!@#$%^&*()\-_=+\[\]{};:\'",.<>?/\\|`~]', password): return 'Password must contain at least one special character.' + # Hash comparison is the most expensive check, so it runs last — + # only after every cheap format check has already passed. + if current_hash: + from werkzeug.security import check_password_hash + if check_password_hash(current_hash, password): + return 'New password must be different from your current password.' return None diff --git a/app/templates/admin/create_user.html b/app/templates/admin/create_user.html index c656061..3fd91d2 100644 --- a/app/templates/admin/create_user.html +++ b/app/templates/admin/create_user.html @@ -124,7 +124,7 @@ -
+
@@ -136,7 +136,7 @@
-