# frozen_string_literal: true module Maquina ## # A concern that implements password history and reuse prevention. # # == Usage # # Include this concern in models that need password history tracking: # # class User < ApplicationRecord # include Maquina::RetainPasswords # end # # == Configuration # # Password retention behavior is controlled by: # - +Maquina.configuration.password_retain_count+:: Number of previous passwords to retain # # == Callbacks # # When included, automatically adds: # - Validation to prevent password reuse # - After create: Stores initial password in history # - After update: Stores new password in history when password changes # # == Validations # # - +password+:: Must not match any previously used passwords within retention limit # # == Example # # class User < ApplicationRecord # include Maquina::RetainPasswords # has_secure_password # end # # user.update(password: 'old_password') # Stored in history # user.update(password: 'old_password') # Validation error: password already used # module RetainPasswords extend ActiveSupport::Concern included do validate :password_uniqueness, if: ->(user) { user.password_digest_changed? } after_create :store_password_digest after_update :store_password_digest, if: ->(user) { user.previous_changes.has_key?(:password_digest) } private # Validates that the new password hasn't been used before # # Compares the new password against stored password history # Adds error if password was previously used within retention limit def password_uniqueness return if Maquina.configuration.password_retain_count.blank? || Maquina.configuration.password_retain_count.zero? used_before = Maquina::UsedPassword.where(user: self).detect do |used_password| bcrypt = ::BCrypt::Password.new(used_password.password_digest) hashed_value = ::BCrypt::Engine.hash_secret(password, bcrypt.salt) ActiveSupport::SecurityUtils.secure_compare(hashed_value, used_password.password_digest) end errors.add(:password, :password_already_used) if used_before.present? end # Stores the current password digest in password history # # Delegates to UsedPassword to handle storage and history maintenance def store_password_digest Maquina::UsedPassword.store_password_digest(id, password_digest) end end end end