From db908e3109e86e17ebb02e11cbf1061df461f2f0 Mon Sep 17 00:00:00 2001 From: Emiliano Frascaro Date: Sat, 8 Aug 2026 11:30:37 +0200 Subject: [PATCH] =?UTF-8?q?Rafforza=20i=20requisiti=20password=20con=20reg?= =?UTF-8?q?ole=20di=20complessit=C3=A0=20di=20mercato.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../controllers/admin/passwords_controller.rb | 13 +++-- .../controllers/api/v1/accounts_controller.rb | 3 + .../public/password_resets_controller.rb | 8 +-- backend/app/models/admin_account.rb | 2 + .../models/concerns/password_complexity.rb | 55 +++++++++++++++++++ backend/app/models/user.rb | 2 + backend/app/services/users/change_password.rb | 14 +++-- .../app/views/public/accounts/show.html.erb | 1 + .../public/password_resets/edit.html.erb | 1 + .../views/public/registrations/new.html.erb | 1 + backend/config/locales/admin.de.yml | 4 +- backend/config/locales/admin.en.yml | 4 +- backend/config/locales/admin.es.yml | 4 +- backend/config/locales/admin.fr.yml | 4 +- backend/config/locales/admin.it.yml | 4 +- backend/config/locales/app.de.yml | 22 ++++++++ backend/config/locales/app.en.yml | 22 ++++++++ backend/config/locales/app.es.yml | 22 ++++++++ backend/config/locales/app.fr.yml | 22 ++++++++ backend/config/locales/app.it.yml | 27 +++++++++ backend/config/locales/ui.de.yml | 4 +- backend/config/locales/ui.en.yml | 4 +- backend/config/locales/ui.es.yml | 4 +- backend/config/locales/ui.fr.yml | 4 +- backend/config/locales/ui.it.yml | 4 +- .../spec/models/password_complexity_spec.rb | 36 ++++++++++++ backend/spec/requests/account_spec.rb | 27 ++++++--- backend/spec/requests/auth_spec.rb | 4 +- native/android/.gitignore | 1 + .../app/src/main/res/values-de/strings.xml | 2 +- .../app/src/main/res/values-en/strings.xml | 2 +- .../app/src/main/res/values-es/strings.xml | 2 +- .../app/src/main/res/values-fr/strings.xml | 2 +- .../app/src/main/res/values/strings.xml | 2 +- native/ios/MatchLiveTv/Core/AppLanguage.swift | 10 ++-- 35 files changed, 296 insertions(+), 47 deletions(-) create mode 100644 backend/app/models/concerns/password_complexity.rb create mode 100644 backend/spec/models/password_complexity_spec.rb diff --git a/backend/app/controllers/admin/passwords_controller.rb b/backend/app/controllers/admin/passwords_controller.rb index 60b34d3..31a6d8d 100644 --- a/backend/app/controllers/admin/passwords_controller.rb +++ b/backend/app/controllers/admin/passwords_controller.rb @@ -9,13 +9,18 @@ module Admin return render :edit, status: :unprocessable_entity end - if params[:password].blank? || params[:password].length < 8 - flash.now[:alert] = t("admin.flash.password_too_short") + if params[:password] != params[:password_confirmation] + flash.now[:alert] = t("admin.flash.password_mismatch") return render :edit, status: :unprocessable_entity end - if params[:password] != params[:password_confirmation] - flash.now[:alert] = t("admin.flash.password_mismatch") + if (code = PasswordComplexity.violation(params[:password])) + key = case code + when :blank, :too_short then :password_too_short + when :too_long then :password_too_long + else :password_too_weak + end + flash.now[:alert] = t("admin.flash.#{key}") return render :edit, status: :unprocessable_entity end diff --git a/backend/app/controllers/api/v1/accounts_controller.rb b/backend/app/controllers/api/v1/accounts_controller.rb index fc03a72..0f6b438 100644 --- a/backend/app/controllers/api/v1/accounts_controller.rb +++ b/backend/app/controllers/api/v1/accounts_controller.rb @@ -48,6 +48,9 @@ module Api case code when :current_incorrect then "Current password is incorrect" when :too_short then "Password must be at least 8 characters" + when :too_long then "Password cannot exceed 72 characters" + when :too_weak + "Password must include at least 3 of: lowercase, uppercase, number, symbol" 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 5e3b9f1..7bb941b 100644 --- a/backend/app/controllers/public/password_resets_controller.rb +++ b/backend/app/controllers/public/password_resets_controller.rb @@ -28,14 +28,14 @@ module Public return end - if params[:password].blank? || params[:password].length < 8 - flash.now[:alert] = t("flash.password_resets.password_min_length") + if params[:password] != params[:password_confirmation] + flash.now[:alert] = t("flash.password_resets.password_mismatch") @token = params[:token] return render :edit, status: :unprocessable_entity end - if params[:password] != params[:password_confirmation] - flash.now[:alert] = t("flash.password_resets.password_mismatch") + if (code = PasswordComplexity.violation(params[:password])) + flash.now[:alert] = t("flash.password_resets.password_#{code == :blank ? :too_short : code}") @token = params[:token] return render :edit, status: :unprocessable_entity end diff --git a/backend/app/models/admin_account.rb b/backend/app/models/admin_account.rb index 37fdc0a..92b722c 100644 --- a/backend/app/models/admin_account.rb +++ b/backend/app/models/admin_account.rb @@ -1,4 +1,6 @@ class AdminAccount < ApplicationRecord + include PasswordComplexity + has_secure_password validates :username, presence: true, uniqueness: true diff --git a/backend/app/models/concerns/password_complexity.rb b/backend/app/models/concerns/password_complexity.rb new file mode 100644 index 0000000..f07a261 --- /dev/null +++ b/backend/app/models/concerns/password_complexity.rb @@ -0,0 +1,55 @@ +# Criteri "di mercato" (stile Cognito/Auth0 bilanciato): +# - minimo 8 caratteri (max 72 per bcrypt) +# - almeno 3 classi su 4: minuscole, maiuscole, numeri, simboli +module PasswordComplexity + extend ActiveSupport::Concern + + MIN_LENGTH = 8 + MAX_LENGTH = 72 + REQUIRED_CLASSES = 3 + + CLASS_CHECKS = { + lowercase: /[a-z]/, + uppercase: /[A-Z]/, + digit: /\d/, + symbol: /[^A-Za-z0-9]/ + }.freeze + + class << self + def violation(password) + value = password.to_s + return :blank if value.blank? + return :too_short if value.length < MIN_LENGTH + return :too_long if value.bytesize > MAX_LENGTH + return :too_weak unless strong_enough?(value) + + nil + end + + def strong_enough?(password) + matched = CLASS_CHECKS.count { |_, pattern| password.match?(pattern) } + matched >= REQUIRED_CLASSES + end + + def requirement_summary + I18n.t("password_policy.hint") + end + end + + included do + validate :password_meets_complexity_policy, if: -> { password.present? } + end + + private + + def password_meets_complexity_policy + case PasswordComplexity.violation(password) + when :too_short + errors.add(:password, :too_short, count: MIN_LENGTH) + when :too_long + errors.add(:password, :too_long, count: MAX_LENGTH) + when :too_weak + errors.add(:password, :complexity) + end + end +end diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 9e26314..bb3adc6 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -1,4 +1,6 @@ class User < ApplicationRecord + include PasswordComplexity + ROLES = %w[admin coach parent volunteer].freeze has_secure_password diff --git a/backend/app/services/users/change_password.rb b/backend/app/services/users/change_password.rb index 5351bc0..ce1f22b 100644 --- a/backend/app/services/users/change_password.rb +++ b/backend/app/services/users/change_password.rb @@ -23,15 +23,19 @@ module Users return Result.new(ok?: false, error: :current_incorrect) end - if @password.blank? || @password.length < 8 - return Result.new(ok?: false, error: :too_short) - end - if @password != @password_confirmation return Result.new(ok?: false, error: :mismatch) end - @user.update!(password: @password) + if (code = PasswordComplexity.violation(@password)) + return Result.new(ok?: false, error: code == :blank ? :too_short : code) + 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) + end + @user.clear_password_reset! Result.new(ok?: true) end diff --git a/backend/app/views/public/accounts/show.html.erb b/backend/app/views/public/accounts/show.html.erb index e715c4c..cad7696 100644 --- a/backend/app/views/public/accounts/show.html.erb +++ b/backend/app/views/public/accounts/show.html.erb @@ -30,6 +30,7 @@

<%= t("auth.account.password_heading") %>

+

<%= t("password_policy.hint") %>

<%= form_with url: public_account_password_path, method: :patch, local: true do %> <%= render "shared/input_toggle", name: :current_password, diff --git a/backend/app/views/public/password_resets/edit.html.erb b/backend/app/views/public/password_resets/edit.html.erb index 72db3ee..7fc0b25 100644 --- a/backend/app/views/public/password_resets/edit.html.erb +++ b/backend/app/views/public/password_resets/edit.html.erb @@ -4,6 +4,7 @@

<%= t("auth.password_reset.title") %>

+

<%= t("password_policy.hint") %>

<%= form_with url: public_password_reset_path, method: :patch, local: true do %> <%= hidden_field_tag :token, @token %> <%= render "shared/input_toggle", diff --git a/backend/app/views/public/registrations/new.html.erb b/backend/app/views/public/registrations/new.html.erb index e20d0e2..209dd42 100644 --- a/backend/app/views/public/registrations/new.html.erb +++ b/backend/app/views/public/registrations/new.html.erb @@ -25,6 +25,7 @@ required: true, minlength: 8, autocomplete: "new-password" %> +

<%= t("password_policy.hint") %>

<%= render "shared/input_toggle", name: "user[password_confirmation]", id: "user_password_confirmation", diff --git a/backend/config/locales/admin.de.yml b/backend/config/locales/admin.de.yml index e3e45a3..1b8b270 100644 --- a/backend/config/locales/admin.de.yml +++ b/backend/config/locales/admin.de.yml @@ -18,6 +18,8 @@ de: logout_success: Abgemeldet password_current_incorrect: Das aktuelle Passwort ist falsch 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_mismatch: Die Passwörter stimmen nicht überein password_updated: Passwort aktualisiert ops_acknowledged: Vorfall übernommen @@ -53,7 +55,7 @@ de: edit: title: Passwort ändern current_password_label: Aktuelles Passwort - new_password_label: "Neues Passwort (mind. 8 Zeichen)" + new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)" confirm_password_label: Neues Passwort bestätigen submit: Passwort speichern cancel: Abbrechen diff --git a/backend/config/locales/admin.en.yml b/backend/config/locales/admin.en.yml index 8ef9e23..8f2d686 100644 --- a/backend/config/locales/admin.en.yml +++ b/backend/config/locales/admin.en.yml @@ -18,6 +18,8 @@ en: logout_success: Signed out password_current_incorrect: Current password is incorrect 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_mismatch: Passwords do not match password_updated: Password updated ops_acknowledged: Incident acknowledged @@ -53,7 +55,7 @@ en: edit: title: Change password current_password_label: Current password - new_password_label: "New password (min. 8 characters)" + new_password_label: "New password (min. 8, at least 3 character types)" confirm_password_label: Confirm new password submit: Save password cancel: Cancel diff --git a/backend/config/locales/admin.es.yml b/backend/config/locales/admin.es.yml index 728bc45..5342ad9 100644 --- a/backend/config/locales/admin.es.yml +++ b/backend/config/locales/admin.es.yml @@ -18,6 +18,8 @@ es: logout_success: Sesión cerrada password_current_incorrect: La contraseña actual no es correcta 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_mismatch: Las contraseñas no coinciden password_updated: Contraseña actualizada ops_acknowledged: Incidencia asumida @@ -53,7 +55,7 @@ es: edit: title: Cambiar contraseña current_password_label: Contraseña actual - new_password_label: "Nueva contraseña (mín. 8 caracteres)" + new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)" confirm_password_label: Confirma la nueva contraseña submit: Guardar contraseña cancel: Cancelar diff --git a/backend/config/locales/admin.fr.yml b/backend/config/locales/admin.fr.yml index 68725e6..d64e47c 100644 --- a/backend/config/locales/admin.fr.yml +++ b/backend/config/locales/admin.fr.yml @@ -18,6 +18,8 @@ fr: logout_success: Déconnecté password_current_incorrect: Le mot de passe actuel est incorrect 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_mismatch: Les mots de passe ne correspondent pas password_updated: Mot de passe mis à jour ops_acknowledged: Incident pris en charge @@ -53,7 +55,7 @@ fr: edit: title: Changer le mot de passe current_password_label: Mot de passe actuel - new_password_label: "Nouveau mot de passe (min. 8 caractères)" + new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)" confirm_password_label: Confirmer le nouveau mot de passe submit: Enregistrer le mot de passe cancel: Annuler diff --git a/backend/config/locales/admin.it.yml b/backend/config/locales/admin.it.yml index 466c08c..f70e2f5 100644 --- a/backend/config/locales/admin.it.yml +++ b/backend/config/locales/admin.it.yml @@ -18,6 +18,8 @@ it: logout_success: Disconnesso password_current_incorrect: Password attuale non corretta 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_mismatch: Le password non coincidono password_updated: Password aggiornata ops_acknowledged: Incidente preso in carico @@ -53,7 +55,7 @@ it: edit: title: Cambia password current_password_label: Password attuale - new_password_label: "Nuova password (min. 8 caratteri)" + new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)" confirm_password_label: Conferma nuova password submit: Salva password cancel: Annulla diff --git a/backend/config/locales/app.de.yml b/backend/config/locales/app.de.yml index 68b778e..1e71300 100644 --- a/backend/config/locales/app.de.yml +++ b/backend/config/locales/app.de.yml @@ -1,4 +1,21 @@ de: + password_policy: + hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen." + activerecord: + errors: + models: + user: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten" + admin_account: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten" club: back_to_club: "← Verein" sport_label: Hauptsportart @@ -604,6 +621,9 @@ de: email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts. invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an. password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein + 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_mismatch: Die Passwörter stimmen nicht überein password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden. accounts: @@ -611,6 +631,8 @@ de: profile_updated: Profil aktualisiert. password_current_incorrect: Das aktuelle Passwort ist falsch 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_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 b5c9355..7c2f9db 100644 --- a/backend/config/locales/app.en.yml +++ b/backend/config/locales/app.en.yml @@ -1,4 +1,21 @@ en: + password_policy: + hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols." + activerecord: + errors: + models: + user: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols" + admin_account: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols" club: back_to_club: "← Club" sport_label: Main sport @@ -604,6 +621,9 @@ en: email_sent: If the email is registered, you'll receive a password reset link shortly. invalid_or_expired_link: Invalid or expired link. Request a new password reset. password_min_length: Password must be at least 8 characters + 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_mismatch: Passwords don't match password_updated: Password updated. You can now log in. accounts: @@ -611,6 +631,8 @@ en: profile_updated: Profile updated. password_current_incorrect: Current password is incorrect 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_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 d1d984b..476e945 100644 --- a/backend/config/locales/app.es.yml +++ b/backend/config/locales/app.es.yml @@ -1,4 +1,21 @@ es: + password_policy: + hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos." + activerecord: + errors: + models: + user: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos" + admin_account: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos" club: back_to_club: "← Club" sport_label: Deporte principal @@ -604,6 +621,9 @@ es: email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña. invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña. password_min_length: La contraseña debe tener al menos 8 caracteres + 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_mismatch: Las contraseñas no coinciden password_updated: Contraseña actualizada. Ya puedes iniciar sesión. accounts: @@ -611,6 +631,8 @@ es: profile_updated: Perfil actualizado. password_current_incorrect: La contraseña actual no es correcta 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_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 bd8a4f0..d34f3a1 100644 --- a/backend/config/locales/app.fr.yml +++ b/backend/config/locales/app.fr.yml @@ -1,4 +1,21 @@ fr: + password_policy: + hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles." + activerecord: + errors: + models: + user: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles" + admin_account: + attributes: + password: + too_short: "is too short (minimum is %{count} characters)" + too_long: "is too long (maximum is %{count} characters)" + complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles" club: back_to_club: "← Club" sport_label: Sport principal @@ -604,6 +621,9 @@ fr: email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe. invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe. password_min_length: Le mot de passe doit comporter au moins 8 caractères + 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_mismatch: Les mots de passe ne correspondent pas password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter. accounts: @@ -611,6 +631,8 @@ fr: profile_updated: Profil mis à jour. password_current_incorrect: Le mot de passe actuel est incorrect 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_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 eee0e1d..613df5e 100644 --- a/backend/config/locales/app.it.yml +++ b/backend/config/locales/app.it.yml @@ -1,4 +1,21 @@ it: + password_policy: + hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli." + activerecord: + errors: + models: + user: + attributes: + password: + too_short: "è troppo corta (minimo %{count} caratteri)" + too_long: "è troppo lunga (massimo %{count} caratteri)" + complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli" + admin_account: + attributes: + password: + too_short: "è troppo corta (minimo %{count} caratteri)" + too_long: "è troppo lunga (massimo %{count} caratteri)" + complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli" club: back_to_club: "← Società" sport_label: Sport principale @@ -604,6 +621,12 @@ it: email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password. invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password. password_min_length: La password deve avere almeno 8 caratteri + 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_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_mismatch: Le password non coincidono password_updated: Password aggiornata. Ora puoi accedere. accounts: @@ -611,6 +634,10 @@ it: profile_updated: Profilo aggiornato. password_current_incorrect: La password attuale non è corretta 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_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 password_updated: Password aggiornata. replay: diff --git a/backend/config/locales/ui.de.yml b/backend/config/locales/ui.de.yml index 9c3231a..88cf26c 100644 --- a/backend/config/locales/ui.de.yml +++ b/backend/config/locales/ui.de.yml @@ -103,7 +103,7 @@ de: password_reset: meta_title: "Neues Passwort — Match Live TV" title: Neues Passwort wählen - new_password_label: "Neues Passwort (min. 8 Zeichen)" + new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)" submit: Passwort speichern back_to_login: Zurück zur Anmeldung invitation: @@ -128,7 +128,7 @@ de: name_label: Name role_label: "Rolle: %{role}" current_password_label: Aktuelles Passwort - new_password_label: "Neues Passwort (mind. 8 Zeichen)" + new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)" save_profile: Profil speichern save_password: Passwort aktualisieren common: diff --git a/backend/config/locales/ui.en.yml b/backend/config/locales/ui.en.yml index ea1e0ef..114682c 100644 --- a/backend/config/locales/ui.en.yml +++ b/backend/config/locales/ui.en.yml @@ -103,7 +103,7 @@ en: password_reset: meta_title: "New password — Match Live TV" title: Choose a new password - new_password_label: "New password (min. 8 characters)" + new_password_label: "New password (min. 8, at least 3 character types)" submit: Save password back_to_login: Back to login invitation: @@ -128,7 +128,7 @@ en: name_label: Name role_label: "Role: %{role}" current_password_label: Current password - new_password_label: "New password (min. 8 characters)" + new_password_label: "New password (min. 8, at least 3 character types)" save_profile: Save profile save_password: Update password common: diff --git a/backend/config/locales/ui.es.yml b/backend/config/locales/ui.es.yml index d230d9a..a4b3e74 100644 --- a/backend/config/locales/ui.es.yml +++ b/backend/config/locales/ui.es.yml @@ -103,7 +103,7 @@ es: password_reset: meta_title: "Nueva contraseña — Match Live TV" title: Elige una nueva contraseña - new_password_label: "Nueva contraseña (mín. 8 caracteres)" + new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)" submit: Guardar contraseña back_to_login: Volver al inicio de sesión invitation: @@ -128,7 +128,7 @@ es: name_label: Nombre role_label: "Rol: %{role}" current_password_label: Contraseña actual - new_password_label: "Nueva contraseña (mín. 8 caracteres)" + new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)" save_profile: Guardar perfil save_password: Actualizar contraseña common: diff --git a/backend/config/locales/ui.fr.yml b/backend/config/locales/ui.fr.yml index 4711125..7072601 100644 --- a/backend/config/locales/ui.fr.yml +++ b/backend/config/locales/ui.fr.yml @@ -103,7 +103,7 @@ fr: password_reset: meta_title: "Nouveau mot de passe — Match Live TV" title: Choisissez un nouveau mot de passe - new_password_label: "Nouveau mot de passe (8 caractères min.)" + new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)" submit: Enregistrer le mot de passe back_to_login: Retour à la connexion invitation: @@ -128,7 +128,7 @@ fr: name_label: Nom role_label: "Rôle : %{role}" current_password_label: Mot de passe actuel - new_password_label: "Nouveau mot de passe (min. 8 caractères)" + new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)" save_profile: Enregistrer le profil save_password: Mettre à jour le mot de passe common: diff --git a/backend/config/locales/ui.it.yml b/backend/config/locales/ui.it.yml index 91e5a68..daf409c 100644 --- a/backend/config/locales/ui.it.yml +++ b/backend/config/locales/ui.it.yml @@ -103,7 +103,7 @@ it: password_reset: meta_title: "Nuova password — Match Live TV" title: Scegli una nuova password - new_password_label: "Nuova password (min. 8 caratteri)" + new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)" submit: Salva password back_to_login: Torna al login invitation: @@ -128,7 +128,7 @@ it: name_label: Nome role_label: "Ruolo: %{role}" current_password_label: Password attuale - new_password_label: "Nuova password (min. 8 caratteri)" + new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)" save_profile: Salva profilo save_password: Aggiorna password common: diff --git a/backend/spec/models/password_complexity_spec.rb b/backend/spec/models/password_complexity_spec.rb new file mode 100644 index 0000000..12b6634 --- /dev/null +++ b/backend/spec/models/password_complexity_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +RSpec.describe PasswordComplexity do + describe ".violation" do + it "accepts a password with 3 character classes" do + expect(described_class.violation("NewPass123")).to be_nil + end + + it "accepts symbols instead of one letter class" do + expect(described_class.violation("newpass1!")).to be_nil + end + + it "rejects passwords that are too short" do + expect(described_class.violation("Ab1!")).to eq(:too_short) + end + + it "rejects passwords with fewer than 3 classes" do + expect(described_class.violation("password123")).to eq(:too_weak) + expect(described_class.violation("PASSWORD123")).to eq(:too_weak) + expect(described_class.violation("Password")).to eq(:too_weak) + 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") + expect(user).not_to be_valid + expect(user.errors[:password]).to be_present + end + + it "allows strong passwords on create" do + user = User.new(email: "strong@example.com", name: "Strong", password: "NewPass123", role: "coach") + expect(user).to be_valid + end + end +end diff --git a/backend/spec/requests/account_spec.rb b/backend/spec/requests/account_spec.rb index a008fdb..eb9d98c 100644 --- a/backend/spec/requests/account_spec.rb +++ b/backend/spec/requests/account_spec.rb @@ -1,9 +1,9 @@ require "rails_helper" RSpec.describe "Account API", type: :request do - let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") } + let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "Password123", role: "coach") } let(:auth_headers) do - post "/api/v1/auth/login", params: { email: user.email, password: "password123" } + post "/api/v1/auth/login", params: { email: user.email, password: "Password123" } token = JSON.parse(response.body).fetch("access_token") { "Authorization" => "Bearer #{token}" } end @@ -40,25 +40,38 @@ RSpec.describe "Account API", type: :request do it "changes the password with the current password" do patch "/api/v1/account/password", params: { - current_password: "password123", - password: "newpass123", - password_confirmation: "newpass123" + current_password: "Password123", + password: "NewPass123", + password_confirmation: "NewPass123" }, headers: auth_headers expect(response).to have_http_status(:ok) - expect(user.reload.authenticate("newpass123")).to be_truthy + expect(user.reload.authenticate("NewPass123")).to be_truthy end it "rejects an incorrect current password" do patch "/api/v1/account/password", params: { current_password: "wrong", + password: "NewPass123", + password_confirmation: "NewPass123" + }, + headers: auth_headers + expect(response).to have_http_status(:unprocessable_entity) + expect(user.reload.authenticate("Password123")).to be_truthy + end + + it "rejects a password that fails complexity rules" do + patch "/api/v1/account/password", + params: { + current_password: "Password123", password: "newpass123", password_confirmation: "newpass123" }, headers: auth_headers expect(response).to have_http_status(:unprocessable_entity) - expect(user.reload.authenticate("password123")).to be_truthy + expect(JSON.parse(response.body)["error"]).to match(/3 of/i) + expect(user.reload.authenticate("Password123")).to be_truthy end end diff --git a/backend/spec/requests/auth_spec.rb b/backend/spec/requests/auth_spec.rb index ea29b2e..26c43bf 100644 --- a/backend/spec/requests/auth_spec.rb +++ b/backend/spec/requests/auth_spec.rb @@ -1,10 +1,10 @@ require "rails_helper" RSpec.describe "Auth API", type: :request do - let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "password123", role: "coach") } + let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "Password123", role: "coach") } it "logs in with valid credentials" do - post "/api/v1/auth/login", params: { email: user.email, password: "password123" } + post "/api/v1/auth/login", params: { email: user.email, password: "Password123" } expect(response).to have_http_status(:ok) expect(JSON.parse(response.body)).to have_key("access_token") end diff --git a/native/android/.gitignore b/native/android/.gitignore index f2cfc63..4f6e5dd 100644 --- a/native/android/.gitignore +++ b/native/android/.gitignore @@ -5,6 +5,7 @@ *.iml /captures/ /app/build/ +/dist/ .DS_Store keystore.properties keystore/ diff --git a/native/android/app/src/main/res/values-de/strings.xml b/native/android/app/src/main/res/values-de/strings.xml index 444ac2c..34dba73 100644 --- a/native/android/app/src/main/res/values-de/strings.xml +++ b/native/android/app/src/main/res/values-de/strings.xml @@ -252,7 +252,7 @@ Dies kann nicht rückgängig gemacht werden. Name Rolle: %1$s Aktuelles Passwort - Neues Passwort (mind. 8 Zeichen) + Neues Passwort (mind. 8, mindestens 3 Zeichenarten) Passwort bestätigen Profil speichern Passwort aktualisieren diff --git a/native/android/app/src/main/res/values-en/strings.xml b/native/android/app/src/main/res/values-en/strings.xml index d6148fb..7612762 100644 --- a/native/android/app/src/main/res/values-en/strings.xml +++ b/native/android/app/src/main/res/values-en/strings.xml @@ -252,7 +252,7 @@ This cannot be undone. Name Role: %1$s Current password - New password (min. 8 characters) + New password (min. 8, at least 3 character types) Confirm password Save profile Update password diff --git a/native/android/app/src/main/res/values-es/strings.xml b/native/android/app/src/main/res/values-es/strings.xml index 38b39e8..70f3564 100644 --- a/native/android/app/src/main/res/values-es/strings.xml +++ b/native/android/app/src/main/res/values-es/strings.xml @@ -252,7 +252,7 @@ Esta acción no se puede deshacer. Nombre Rol: %1$s Contraseña actual - Nueva contraseña (mín. 8 caracteres) + Nueva contraseña (mín. 8, al menos 3 tipos de caracteres) Confirmar contraseña Guardar perfil Actualizar contraseña diff --git a/native/android/app/src/main/res/values-fr/strings.xml b/native/android/app/src/main/res/values-fr/strings.xml index bdd5c90..6a4a6fd 100644 --- a/native/android/app/src/main/res/values-fr/strings.xml +++ b/native/android/app/src/main/res/values-fr/strings.xml @@ -252,7 +252,7 @@ Cette action est irréversible. Nom Rôle : %1$s Mot de passe actuel - Nouveau mot de passe (min. 8 caractères) + Nouveau mot de passe (min. 8, au moins 3 types de caractères) Confirmer le mot de passe Enregistrer le profil Mettre à jour le mot de passe diff --git a/native/android/app/src/main/res/values/strings.xml b/native/android/app/src/main/res/values/strings.xml index 508eb18..15842f1 100644 --- a/native/android/app/src/main/res/values/strings.xml +++ b/native/android/app/src/main/res/values/strings.xml @@ -252,7 +252,7 @@ L\'operazione non si può annullare. Nome Ruolo: %1$s Password attuale - Nuova password (min. 8 caratteri) + Nuova password (min. 8, almeno 3 tipi di caratteri) Conferma password Salva profilo Aggiorna password diff --git a/native/ios/MatchLiveTv/Core/AppLanguage.swift b/native/ios/MatchLiveTv/Core/AppLanguage.swift index f2782f3..3cf4b5b 100644 --- a/native/ios/MatchLiveTv/Core/AppLanguage.swift +++ b/native/ios/MatchLiveTv/Core/AppLanguage.swift @@ -81,7 +81,7 @@ enum L10n { "account.confirm.password": "Conferma password", "account.current.password": "Password attuale", "account.name.label": "Nome", - "account.new.password": "Nuova password (min. 8 caratteri)", + "account.new.password": "Nuova password (min. 8, almeno 3 tipi di caratteri)", "account.password.heading": "Cambia password", "account.password.saved": "Password aggiornata", "account.profile.heading": "Profilo", @@ -372,7 +372,7 @@ enum L10n { "account.confirm.password": "Confirm password", "account.current.password": "Current password", "account.name.label": "Name", - "account.new.password": "New password (min. 8 characters)", + "account.new.password": "New password (min. 8, at least 3 character types)", "account.password.heading": "Change password", "account.password.saved": "Password updated", "account.profile.heading": "Profile", @@ -663,7 +663,7 @@ enum L10n { "account.confirm.password": "Confirmer le mot de passe", "account.current.password": "Mot de passe actuel", "account.name.label": "Nom", - "account.new.password": "Nouveau mot de passe (min. 8 caractères)", + "account.new.password": "Nouveau mot de passe (min. 8, au moins 3 types de caractères)", "account.password.heading": "Changer le mot de passe", "account.password.saved": "Mot de passe mis à jour", "account.profile.heading": "Profil", @@ -954,7 +954,7 @@ enum L10n { "account.confirm.password": "Passwort bestätigen", "account.current.password": "Aktuelles Passwort", "account.name.label": "Name", - "account.new.password": "Neues Passwort (mind. 8 Zeichen)", + "account.new.password": "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)", "account.password.heading": "Passwort ändern", "account.password.saved": "Passwort aktualisiert", "account.profile.heading": "Profil", @@ -1245,7 +1245,7 @@ enum L10n { "account.confirm.password": "Confirmar contraseña", "account.current.password": "Contraseña actual", "account.name.label": "Nombre", - "account.new.password": "Nueva contraseña (mín. 8 caracteres)", + "account.new.password": "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)", "account.password.heading": "Cambiar contraseña", "account.password.saved": "Contraseña actualizada", "account.profile.heading": "Perfil",