robertgauld/OSMExtender

View on GitHub
app/models/user.rb

Summary

Maintainability
A
1 hr
Test Coverage
class User < ApplicationRecord
  authenticates_with_sorcery!
  has_paper_trail(
    :skip => [:crypted_password, :salt, :activation_token, :reset_password_token],
    :on => [:create, :update] # not :destroy, :touch
  )

  has_many :email_reminders, dependent: :destroy, inverse_of: :user
  has_many :email_reminder_shares, through: :email_reminders, source: :shares
  has_many :email_lists, dependent: :destroy, inverse_of: :user
  has_many :automation_tasks, dependent: :destroy, inverse_of: :user
  has_many :hidden_announcements, dependent: :destroy, inverse_of: :user
  has_many :emailed_announcements, dependent: :destroy, inverse_of: :user
  has_many :usage_log, inverse_of: :user

  scope :activated, -> { where(activation_state: 'active') }
  scope :unactivated, -> { where(activation_state: 'pending') }
  scope :activation_expired, -> { unactivated.where("activation_token_expires_at < ?", Time.now) }
  scope :connected_to_osm, -> { where.not(osm_userid: nil, osm_secret: nil) }
  scope :not_connected_to_osm, -> { where(osm_userid: nil, osm_secret: nil) }

  validates_presence_of :name

  validates_presence_of :email_address
  validates_uniqueness_of :email_address, :case_sensitive => false
  validates :email_address, :email_format => true

  validates_presence_of :password, :unless => Proc.new { |record| record.send(sorcery_config.password_attribute_name).nil? }
  validates_confirmation_of :password, :unless => Proc.new { |record| record.send(sorcery_config.password_attribute_name).nil? }
  validate :password_complexity, :password_not_email_address, :password_not_name, :unless => Proc.new { |record| record.send(sorcery_config.password_attribute_name).nil? }

  validates_numericality_of :startup_section, :only_integer=>true, :greater_than_or_equal_to=>0
  validates_numericality_of :custom_row_height, :only_integer=>true, :greater_than_or_equal_to=>0
  validates_numericality_of :custom_text_size, :only_integer=>true, :greater_than_or_equal_to=>0

  validates_acceptance_of :gdpr_consent, on: :create

  before_save :set_gdpr_consent_timestamp, if: Proc.new { |r| r.gdpr_consent.eql?('1') }
  before_destroy { versions.destroy_all }

  def change_password!(new_password, new_password_confirmation=new_password)
    self.password = new_password
    self.password_confirmation = new_password_confirmation

    if valid? && errors.none? && super(new_password)
      return true
    end
    return false
  end


  def activated?
    send(sorcery_config.activation_state_attribute_name).eql?('active')
  end

  def unactivated?
    send(sorcery_config.activation_state_attribute_name).eql?('pending')
  end


  def connected_to_osm?
    osm_userid.present? && osm_secret.present?
  end

  def not_connected_to_osm?
    !connected_to_osm?
  end

  def connect_to_osm(email, password)
    result = Osm::Api.authorize(email, password)

    write_attribute(:osm_userid, result[:user_id])
    write_attribute(:osm_secret, result[:secret])
    return save
  end

  def osm_api
    if connected_to_osm?
      @osm_api ||= Osm::Api.new(read_attribute(:osm_userid), read_attribute(:osm_secret))
      return @osm_api
    else
      return nil
    end
  end


  def current_announcements
    Announcement.are_current.ignoring(hidden_announcements.pluck(:announcement_id))
  end


  def deliver_reset_password_instructions!(options={})
    config = sorcery_config
    # hammering protection
    return false if config.reset_password_time_between_emails.present? && self.send(config.reset_password_email_sent_at_attribute_name) && self.send(config.reset_password_email_sent_at_attribute_name) > config.reset_password_time_between_emails.seconds.ago.utc
    self.class.sorcery_adapter.transaction do
      generate_reset_password_token!
      if options[:expiration]
        self.reset_password_token_expires_at = options[:expiration].seconds.from_now
        self.save!
      end
      send_reset_password_email! unless config.reset_password_mailer_disabled
    end
  end


  def gravatar_id
    return Digest::MD5.hexdigest(read_attribute(:email_address).downcase)
  end

  def email_address_with_name
    "\"#{name.gsub('"', '')}\" <#{email_address}>"
  end


  def self.search(column, text)
    allowed_columns = [:name, :email_address]

    if !text.blank? && allowed_columns.include?(column)
      text.downcase! if [:email_address].include?(column)
      where(["#{column.to_s} LIKE ?", "%#{text}%"])
    else
      all
    end
  end


  # Check if the user and API have a given OSM permission
  # @param section the section to check
  # @param permission_to the action which is being checked (:read or :write), this can be an array in which case the user must be able to perform all actions to the object
  # @param permission_on the object type which is being checked (:member, :register ...), this can be an array in which case the user must be able to perform the action to all objects
  def has_osm_permission?(section, permission_to, permission_on)
    user_can = user_has_osm_permission?(section, permission_to, permission_on)
    api_can = api_has_osm_permission?(section, permission_to, permission_on)
    user_can && api_can
  end

  # Check if the user has a given OSM permission
  def user_has_osm_permission?(section, permission_to, permission_on)
    permissions = osm_api.get_user_permissions[section.to_i] || {}
    [*permission_on].each do |on|
      [*permission_to].each do |to|
        unless (permissions[on] || []).include?(to)
          return false
        end
      end
    end
    true
  end

  # Check if the API has a given OSM permission
  def api_has_osm_permission?(section, permission_to, permission_on)
    permissions = Osm::ApiAccess.get_ours(osm_api, section).permissions
    [*permission_on].each do |on|
      [*permission_to].each do |to|
        unless (permissions[on] || []).include?(to)
          return false
        end
      end
    end
    true
  end


  private
  # Use Steve Gibson's Password Haystacks logic to ensure password is sufficently secure
  # https://www.grc.com/haystack.htm
  # Assume the following guesses per second:
  #  * Online attack - one thousand (10**3)
  #  * Offline attack - one hundred billion (10**11)
  #  * Cracking array - one hundred trillion (10**14)
  def password_complexity
    minimum_haystack = (10**14) # Withstand for 1 second of a massive cracking array
    pass = send(sorcery_config.password_attribute_name)

    alphabet_size = 0
    alphabet_size += 26 if pass.gsub(/[^a-z]/, '').length > 0
    alphabet_size += 26 if pass.gsub(/[^A-Z]/, '').length > 0
    alphabet_size += 10 if pass.gsub(/[^0-9]/, '').length > 0
    alphabet_size += 33 if pass.gsub(/[a-zA-Z0-9]/, '').length > 0

    haystack_size = alphabet_size**pass.length

    if haystack_size < minimum_haystack
      if alphabet_size < 40
        errors.add(:password, "isn't complex enough, try adding more types of character (upper case, lower case, numeric and symbol)")
      end
      if pass.length < 10
        errors.add(:password, "isn't complex enough, try increasing its length")
      end
    end

    return haystack_size
  end
  
  def password_not_email_address
    if send(sorcery_config.password_attribute_name).downcase.strip.eql?(email_address.downcase.strip)
      errors.add(:password, 'is not allowed to be your email address')
      return false
    end
    return true
  end

  def password_not_name
    block_size = 3
    name = self.name.downcase
    pass = send(sorcery_config.password_attribute_name).downcase
    for i in 0..(name.length - block_size)
      find = name[i..(i+(block_size-1))]
      if pass.include?(find)
        errors.add(:password, 'is not allowed to contain part of your name')
        return false
      end
    end
    return true
  end

  def set_gdpr_consent_timestamp
    return unless gdpr_consent.eql?('1')
    write_attribute :gdpr_consent_at, Time.now.utc
  end

  public
  def clear_reset_password_token
    super
  end

end