lib/rodauth/features/password_expiration.rb



# frozen-string-literal: true

module Rodauth
  Feature.define(:password_expiration, :PasswordExpiration) do
    depends :login, :change_password

    error_flash "Your password has expired and needs to be changed"
    error_flash "Your password cannot be changed yet", 'password_not_changeable_yet'

    redirect :password_not_changeable_yet 
    redirect(:password_change_needed){change_password_path}

    auth_value_method :allow_password_change_after, -86400
    auth_value_method :require_password_change_after, 90*86400
    auth_value_method :password_expiration_table, :account_password_change_times
    auth_value_method :password_expiration_id_column, :id
    auth_value_method :password_expiration_changed_at_column, :changed_at
    session_key :password_changed_at_session_key, :password_changed_at
    auth_value_method :password_expiration_default, false

    auth_methods(
      :password_expired?,
      :update_password_changed_at
    )

    def get_password_changed_at
      convert_timestamp(password_expiration_ds.get(password_expiration_changed_at_column))
    end

    def check_password_change_allowed
      if password_changed_at = get_password_changed_at
        if password_changed_at > Time.now - allow_password_change_after
          set_redirect_error_flash password_not_changeable_yet_error_flash
          redirect password_not_changeable_yet_redirect
        end
      end
    end

    def set_password(password)
      update_password_changed_at
      set_session_value(password_changed_at_session_key, Time.now.to_i)
      super
    end

    def account_from_reset_password_key(key)
      if a = super
        check_password_change_allowed
      end
      a
    end

    def update_password_changed_at
      ds = password_expiration_ds
      if ds.update(password_expiration_changed_at_column=>Sequel::CURRENT_TIMESTAMP) == 0
        # Ignoring the violation is safe here, since a concurrent insert would also set it to the
        # current timestamp.
        ignore_uniqueness_violation{ds.insert(password_expiration_id_column=>account_id)}
      end
    end

    def require_current_password
      if authenticated? && password_expired? && password_change_needed_redirect != request.path_info
        set_redirect_error_flash password_expiration_error_flash
        redirect password_change_needed_redirect
      end
    end

    def password_expired?
      if password_changed_at = session[password_changed_at_session_key]
        return password_changed_at + require_password_change_after < Time.now.to_i
      end

      account_from_session
      if password_changed_at = get_password_changed_at
        set_session_value(password_changed_at_session_key, password_changed_at.to_i)
        password_changed_at + require_password_change_after < Time.now
      else
        set_session_value(password_changed_at_session_key, password_expiration_default ? 0 : 2147483647)
        password_expiration_default
      end
    end

    private

    def after_close_account
      super if defined?(super)
      password_expiration_ds.delete
    end

    def before_change_password_route
      check_password_change_allowed
      super
    end

    def after_create_account
      if account_password_hash_column
        update_password_changed_at
      end
      super if defined?(super)
    end

    def after_login
      require_current_password
      super
    end

    def password_expiration_ds
      db[password_expiration_table].where(password_expiration_id_column=>account_id)
    end
  end
end