openjaf/cenit

View on GitHub
app/models/user.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require 'net/http'
require 'identicon'
require 'mongoid_userstamp'

class User
  include Setup::CenitUnscoped
  include Cenit::MultiTenancy::UserScope
  extend DeviseOverrides
  include CredentialsGenerator
  include FieldsInspection
  include TimeZoneAware
  include ObserverTenantLookup


  inspect_fields :name, :given_name, :family_name, :picture, :encrypted_password,
                 :account_id, :api_account_id, :time_zone

  rolify

  build_in_data_type
    .on_origin(:admin)
    .with(:email, :name, :account, :roles, :super_admin_enabled, :need_password_reset)
    .and(
      label: '{{name}} ({{email}})',
      properties: {
        password: {
          type: 'string'
        }
      }
    )
    .protecting(:password)
  build_in_data_type.bulkable_deletable :denied

  deny :all

  has_many :accounts, class_name: Account.to_s, inverse_of: :owner
  has_and_belongs_to_many :member_accounts, class_name: Account.to_s, inverse_of: :users
  belongs_to :account, class_name: Account.to_s, inverse_of: nil
  belongs_to :api_account, class_name: Account.to_s, inverse_of: nil

  # Include default devise modules. Others available are:
  # :lockable, :timeoutable, :rememberable

  devise :trackable, :validatable, :database_authenticatable, :recoverable, :confirmable
  devise :registerable unless ENV['UNABLE_REGISTERABLE'].to_b
  devise :timeoutable if ENV['SESSION_TIMEOUT'].to_i > 0

  # Database authenticatable
  field :email, type: String, default: ''
  field :encrypted_password, type: String, default: ''

  ## Recoverable
  field :reset_password_token, type: String
  field :reset_password_sent_at, type: Time

  ## Rememberable
  field :remember_created_at, type: Time

  ## Trackable
  field :sign_in_count, type: Integer, default: 0
  field :current_sign_in_at, type: Time
  field :last_sign_in_at, type: Time
  field :confirmed_at, type: Time
  field :confirmation_sent_at, type: Time
  field :confirmation_token, type: String
  field :current_sign_in_ip, type: String
  field :last_sign_in_ip, type: String
  field :unconfirmed_email, type: String

  #Profile
  mount_uploader :picture, ImageUploader
  field :name, type: String
  field :given_name, type: String
  field :family_name, type: String
  field :picture_url, type: String

  field :super_admin_enabled, type: Mongoid::Boolean

  field :need_password_reset, type: Mongoid::Boolean
  field :session_token, type: String

  def authenticatable_salt
    return super unless session_token

    "#{super}#{session_token}"
  end

  def invalidate_all_sessions!
    update(session_token: SecureRandom.hex)
  end

  def inspecting_fields
    super + (has_role?(:super_admin) ? [:super_admin_enabled] : [])
  end

  def check_attributes
    remove_attribute(:super_admin_enabled) unless has_role?(:super_admin)
    if attributes['name'] && attributes['given_name'].present? && attributes['family_name'].present?
      remove_attribute(:name)
    end
    if !new_record? && changed_attributes.key?('encrypted_password')
      remove_attribute(:need_password_reset)
    end
  end

  before_create do
    self.role_ids = ((role_ids || []) + ::Role.default_ids(self.class.empty?)).uniq
    self.account ||= accounts.first || member_accounts.first || Account.current || Account.new_for_create(owner_id: id)
    unless owns?(account)
      errors.add(:account, 'is sealed and can not be inspected') if account&.sealed?
    end
    unless owns?(api_account)
      self.api_account = owns?(account) ? account : accounts.first
    end
    errors.blank?
  end

  before_save :check_attributes, :check_default_roles, :ensure_token, :validates_time_zone!, :check_account

  after_create do
    account.save unless account.persisted?
  end

  def check_default_roles
    role_ids = self.role_ids || []
    if ::Role.default_ids.any? { |id| role_ids.exclude?(id) }
      self.role_ids = (role_ids + ::Role.default_ids).uniq
    end
    abort_if_has_errors
  end

  def check_account
    unless account_id.nil? || super_admin? || accounts.where(id: account_id).exists? || member_account_ids.include?(account_id)
      errors.add(:account, 'is not valid')
    end
    abort_if_has_errors
  end

  def all_accounts
    (accounts + member_accounts).uniq
  end

  def name
    read_attribute(:name) || "#{given_name} #{family_name}".strip.presence
  end

  def short_name
    given_name.presence || name.presence
  end

  def picture_url(size = 50)
    custom_picture_url(size) || read_attribute(:picture_url) || gravatar_or_identicon_url(size)
  end

  def custom_picture_url(_size)
    picture.present? && picture.url
  end

  def user
    self
  end

  def owns?(account)
    !account.nil? && account.owner_id == id
  end

  def member?(account)
    !account.nil? && account.users.map(&:id).include?(id)
  end

  def account_ids #TODO look for usages and try to optimize
    accounts.collect(&:id)
  end

  def label
    if name.present?
      "#{name} (#{email})"
    else
      email
    end
  end

  def method_missing(symbol, *args)
    if (match = symbol.to_s.match(/(.+)\?/))
      has_role?(match[1].to_sym)
    else
      super
    end
  end

  def admin?
    has_role?(:admin) || has_role?(:super_admin)
  end

  def super_admin?
    has_role?(:super_admin) && super_admin_enabled
  end

  def notification_span_for(type)
    type = type.to_s.to_sym
    unless @notification_spans
      @notification_spans = {}
      regex = Regexp.new("\\A(#{Setup::SystemNotification.type_enum.join('|')})_notifications_span\\Z")
      roles.each do |role|
        next unless (metadata = role.metadata).is_a?(Hash)
        metadata.each do |key, value|
          next unless (match = key.to_s.match(regex))
          t = match[1].to_sym
          @notification_spans[t] = [@notification_spans[t] || 0, value.to_i].max
        end
      end
    end
    @notification_spans[type]
  end

  def avatar_id
    email
  end

  def gravatar()
    gravatar_check = "//gravatar.com/avatar/#{Digest::MD5.hexdigest(avatar_id.to_s.downcase)}.png?d=404"
    uri = URI.parse(gravatar_check)
    http = Net::HTTP.new(uri.host, uri.port)
    request = Net::HTTP::Get.new("/avatar/#{Digest::MD5.hexdigest(avatar_id.to_s.downcase)}.png?d=404")
    response = http.request(request)
    response.code.to_i < 400 # from d=404 parameter
  rescue
    false
  end

  def identicon(size = 50)
    Identicon.data_url_for avatar_id.to_s.downcase, size
  end

  def gravatar_or_identicon_url(size = 50)
    if gravatar
      "//gravatar.com/avatar/#{Digest::MD5.hexdigest avatar_id.to_s}?s=#{size}"
    else
      identicon size
    end
  end

  class << self

    def find_where(expression)
      scope = where(expression)
      unless current_super_admin?
        scope = scope.and(id: current&.id)
      end
      scope
    end

    def find_all
      find_where({})
    end

    def method_missing(symbol, *args)
      if (match = symbol.to_s.match(/\Acurrent_(.+)\?/))
        current && current.send("#{match[1]}?")
      else
        super
      end
    end

    def super_access?
      Thread.current[:super_access] || current_super_admin?
    end

    def with_super_access
      current_access = Thread.current[:super_access]
      Thread.current[:super_access] = true
      yield if block_given?
    ensure
      Thread.current[:super_access] = current_access
    end

    def current_number
      current&.number || 'XXXXXXX'
    end

    def current_token
      current&.token || 'XXXXXXXXXXXXXXXX'
    end

    def current_id
      current&.id
    end
  end

  def confirmation_required?
    ENV['CONFIRMATION_REQUIRED'].to_b &&
      (super_method = method(__method__).super_method) &&
      super_method.call
  end

  def switch(&block)
    current = ::User.current
    ::User.current = self
    account.switch(&block)
  ensure
    ::User.current = current if block
  end

  protected :confirmation_required?
end