# 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 # 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 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