diff --git a/backend/app/controllers/admin/passwords_controller.rb b/backend/app/controllers/admin/passwords_controller.rb index 31a6d8d..52742fd 100644 --- a/backend/app/controllers/admin/passwords_controller.rb +++ b/backend/app/controllers/admin/passwords_controller.rb @@ -24,6 +24,11 @@ module Admin return render :edit, status: :unprocessable_entity end + if PasswordComplexity.same_as_current?(current_admin_account, params[:password]) + flash.now[:alert] = t("admin.flash.password_same_as_current") + return render :edit, status: :unprocessable_entity + end + if current_admin_account.update(password: params[:password]) redirect_to admin_root_path, notice: t("admin.flash.password_updated") else diff --git a/backend/app/controllers/api/v1/accounts_controller.rb b/backend/app/controllers/api/v1/accounts_controller.rb index 0f6b438..d903ed3 100644 --- a/backend/app/controllers/api/v1/accounts_controller.rb +++ b/backend/app/controllers/api/v1/accounts_controller.rb @@ -51,6 +51,7 @@ module Api when :too_long then "Password cannot exceed 72 characters" when :too_weak "Password must include at least 3 of: lowercase, uppercase, number, symbol" + when :same_as_current then "New password must be different from the current password" when :mismatch then "Passwords do not match" else "Unable to update password" end diff --git a/backend/app/controllers/public/password_resets_controller.rb b/backend/app/controllers/public/password_resets_controller.rb index 7bb941b..28d62e7 100644 --- a/backend/app/controllers/public/password_resets_controller.rb +++ b/backend/app/controllers/public/password_resets_controller.rb @@ -40,6 +40,12 @@ module Public return render :edit, status: :unprocessable_entity end + if PasswordComplexity.same_as_current?(@user, params[:password]) + flash.now[:alert] = t("flash.password_resets.password_same_as_current") + @token = params[:token] + return render :edit, status: :unprocessable_entity + end + @user.update!(password: params[:password]) @user.clear_password_reset! redirect_to public_login_path, notice: t("flash.password_resets.password_updated") diff --git a/backend/app/models/concerns/password_complexity.rb b/backend/app/models/concerns/password_complexity.rb index f07a261..5c43f32 100644 --- a/backend/app/models/concerns/password_complexity.rb +++ b/backend/app/models/concerns/password_complexity.rb @@ -31,6 +31,13 @@ module PasswordComplexity matched >= REQUIRED_CLASSES end + # Confronta con il digest già salvato (prima di assegnare la nuova password). + def same_as_current?(record, password) + return false if password.blank? || !record.respond_to?(:authenticate) + + record.authenticate(password).present? + end + def requirement_summary I18n.t("password_policy.hint") end diff --git a/backend/app/services/users/change_password.rb b/backend/app/services/users/change_password.rb index ce1f22b..39b57d7 100644 --- a/backend/app/services/users/change_password.rb +++ b/backend/app/services/users/change_password.rb @@ -31,6 +31,10 @@ module Users return Result.new(ok?: false, error: code == :blank ? :too_short : code) end + if PasswordComplexity.same_as_current?(@user, @password) + return Result.new(ok?: false, error: :same_as_current) + end + unless @user.update(password: @password) complexity_error = @user.errors.details[:password]&.any? { |d| d[:error] == :complexity } return Result.new(ok?: false, error: complexity_error ? :too_weak : :too_short) diff --git a/backend/config/locales/admin.de.yml b/backend/config/locales/admin.de.yml index 1b8b270..d65159c 100644 --- a/backend/config/locales/admin.de.yml +++ b/backend/config/locales/admin.de.yml @@ -20,6 +20,7 @@ de: password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein password_too_long: Das neue Passwort darf höchstens 72 Zeichen lang sein password_too_weak: Das neue Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten + password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden password_mismatch: Die Passwörter stimmen nicht überein password_updated: Passwort aktualisiert ops_acknowledged: Vorfall übernommen diff --git a/backend/config/locales/admin.en.yml b/backend/config/locales/admin.en.yml index 8f2d686..1f8d295 100644 --- a/backend/config/locales/admin.en.yml +++ b/backend/config/locales/admin.en.yml @@ -20,6 +20,7 @@ en: password_too_short: The new password must be at least 8 characters long password_too_long: New password cannot exceed 72 characters password_too_weak: "New password must include at least 3 of: lowercase, uppercase, numbers and symbols" + password_same_as_current: New password must be different from the current password password_mismatch: Passwords do not match password_updated: Password updated ops_acknowledged: Incident acknowledged diff --git a/backend/config/locales/admin.es.yml b/backend/config/locales/admin.es.yml index 5342ad9..e1f1c9c 100644 --- a/backend/config/locales/admin.es.yml +++ b/backend/config/locales/admin.es.yml @@ -20,6 +20,7 @@ es: password_too_short: La nueva contraseña debe tener al menos 8 caracteres password_too_long: La nueva contraseña no puede superar los 72 caracteres password_too_weak: "La nueva contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos" + password_same_as_current: La nueva contraseña debe ser distinta de la actual password_mismatch: Las contraseñas no coinciden password_updated: Contraseña actualizada ops_acknowledged: Incidencia asumida diff --git a/backend/config/locales/admin.fr.yml b/backend/config/locales/admin.fr.yml index d64e47c..a9a0915 100644 --- a/backend/config/locales/admin.fr.yml +++ b/backend/config/locales/admin.fr.yml @@ -20,6 +20,7 @@ fr: password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères password_too_long: Le nouveau mot de passe ne peut pas dépasser 72 caractères password_too_weak: "Le nouveau mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles" + password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel" password_mismatch: Les mots de passe ne correspondent pas password_updated: Mot de passe mis à jour ops_acknowledged: Incident pris en charge diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml index f70e2f5..ffdf74e 100644 --- a/backend/config/locales/admin.it.yml +++ b/backend/config/locales/admin.it.yml @@ -20,6 +20,7 @@ it: password_too_short: La nuova password deve avere almeno 8 caratteri password_too_long: La nuova password non può superare i 72 caratteri password_too_weak: "La nuova password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli" + password_same_as_current: La nuova password deve essere diversa da quella attuale password_mismatch: Le password non coincidono password_updated: Password aggiornata ops_acknowledged: Incidente preso in carico diff --git a/backend/config/locales/app.de.yml b/backend/config/locales/app.de.yml index 1e71300..4e6f62c 100644 --- a/backend/config/locales/app.de.yml +++ b/backend/config/locales/app.de.yml @@ -624,6 +624,7 @@ de: password_too_short: Das Passwort muss mindestens 8 Zeichen lang sein password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten + password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden password_mismatch: Die Passwörter stimmen nicht überein password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden. accounts: @@ -633,6 +634,7 @@ de: password_too_short: Das Passwort muss mindestens 8 Zeichen haben password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten + password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden password_mismatch: Die Passwörter stimmen nicht überein password_updated: Passwort aktualisiert. replay: diff --git a/backend/config/locales/app.en.yml b/backend/config/locales/app.en.yml index 7c2f9db..e91282e 100644 --- a/backend/config/locales/app.en.yml +++ b/backend/config/locales/app.en.yml @@ -624,6 +624,7 @@ en: password_too_short: Password must be at least 8 characters password_too_long: Password cannot exceed 72 characters password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols" + password_same_as_current: New password must be different from the current password password_mismatch: Passwords don't match password_updated: Password updated. You can now log in. accounts: @@ -633,6 +634,7 @@ en: password_too_short: Password must be at least 8 characters password_too_long: Password cannot exceed 72 characters password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols" + password_same_as_current: New password must be different from the current password password_mismatch: Passwords do not match password_updated: Password updated. replay: diff --git a/backend/config/locales/app.es.yml b/backend/config/locales/app.es.yml index 476e945..c1ff4ae 100644 --- a/backend/config/locales/app.es.yml +++ b/backend/config/locales/app.es.yml @@ -624,6 +624,7 @@ es: password_too_short: La contraseña debe tener al menos 8 caracteres password_too_long: La contraseña no puede superar los 72 caracteres password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos" + password_same_as_current: La nueva contraseña debe ser distinta de la actual password_mismatch: Las contraseñas no coinciden password_updated: Contraseña actualizada. Ya puedes iniciar sesión. accounts: @@ -633,6 +634,7 @@ es: password_too_short: La contraseña debe tener al menos 8 caracteres password_too_long: La contraseña no puede superar los 72 caracteres password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos" + password_same_as_current: La nueva contraseña debe ser distinta de la actual password_mismatch: Las contraseñas no coinciden password_updated: Contraseña actualizada. replay: diff --git a/backend/config/locales/app.fr.yml b/backend/config/locales/app.fr.yml index d34f3a1..d7fd649 100644 --- a/backend/config/locales/app.fr.yml +++ b/backend/config/locales/app.fr.yml @@ -624,6 +624,7 @@ fr: password_too_short: Le mot de passe doit comporter au moins 8 caractères password_too_long: Le mot de passe ne peut pas dépasser 72 caractères password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles" + password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel" password_mismatch: Les mots de passe ne correspondent pas password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter. accounts: @@ -633,6 +634,7 @@ fr: password_too_short: Le mot de passe doit contenir au moins 8 caractères password_too_long: Le mot de passe ne peut pas dépasser 72 caractères password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles" + password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel" password_mismatch: Les mots de passe ne correspondent pas password_updated: Mot de passe mis à jour. replay: diff --git a/backend/config/locales/app.it.yml b/backend/config/locales/app.it.yml index 613df5e..341e9a1 100644 --- a/backend/config/locales/app.it.yml +++ b/backend/config/locales/app.it.yml @@ -624,6 +624,7 @@ it: password_too_short: La password deve avere almeno 8 caratteri password_too_long: La password non può superare i 72 caratteri password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli" + password_same_as_current: La nuova password deve essere diversa da quella attuale password_too_short: La password deve avere almeno 8 caratteri password_too_long: La password non può superare i 72 caratteri password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli" @@ -636,6 +637,7 @@ it: password_too_short: La password deve avere almeno 8 caratteri password_too_long: La password non può superare i 72 caratteri password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli" + password_same_as_current: La nuova password deve essere diversa da quella attuale password_too_long: La password non può superare i 72 caratteri password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli" password_mismatch: Le password non coincidono diff --git a/backend/spec/models/password_complexity_spec.rb b/backend/spec/models/password_complexity_spec.rb index 12b6634..4e928f9 100644 --- a/backend/spec/models/password_complexity_spec.rb +++ b/backend/spec/models/password_complexity_spec.rb @@ -21,6 +21,15 @@ RSpec.describe PasswordComplexity do end end + describe ".same_as_current?" do + let!(:user) { User.create!(email: "same@example.com", name: "Same", password: "Password123", role: "coach") } + + it "detects when the new password matches the current one" do + expect(described_class.same_as_current?(user, "Password123")).to eq(true) + expect(described_class.same_as_current?(user, "OtherPass123")).to eq(false) + end + end + describe "User validation" do it "blocks weak passwords on create" do user = User.new(email: "weak@example.com", name: "Weak", password: "password123", role: "coach") diff --git a/backend/spec/requests/account_spec.rb b/backend/spec/requests/account_spec.rb index eb9d98c..3a87e8c 100644 --- a/backend/spec/requests/account_spec.rb +++ b/backend/spec/requests/account_spec.rb @@ -73,6 +73,19 @@ RSpec.describe "Account API", type: :request do expect(JSON.parse(response.body)["error"]).to match(/3 of/i) expect(user.reload.authenticate("Password123")).to be_truthy end + + it "rejects reusing the current password" do + patch "/api/v1/account/password", + params: { + current_password: "Password123", + password: "Password123", + password_confirmation: "Password123" + }, + headers: auth_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)["error"]).to match(/different/i) + expect(user.reload.authenticate("Password123")).to be_truthy + end end describe "POST /api/v1/auth/password/forgot" do