1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00
fab-manager/app/models/user.rb

502 lines
16 KiB
Ruby

# frozen_string_literal: true
# User is a physical or moral person with its authentication parameters
# It is linked to the Profile model with hold information about this person (like address, name, etc.)
class User < ApplicationRecord
require 'sso_logger'
include NotifyWith::NotificationReceiver
include NotifyWith::NotificationAttachedObject
# Include default devise modules. Others available are:
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable,
:confirmable
rolify
# enable OmniAuth authentication only if needed
devise :omniauthable, omniauth_providers: [AuthProvider.active.strategy_name.to_sym] unless
AuthProvider.active.providable_type == DatabaseProvider.name
extend FriendlyId
friendly_id :username, use: :slugged
has_one :profile, dependent: :destroy
accepts_nested_attributes_for :profile
has_one :invoicing_profile, dependent: :nullify
accepts_nested_attributes_for :invoicing_profile
has_one :statistic_profile, dependent: :nullify
accepts_nested_attributes_for :statistic_profile
has_many :project_users, dependent: :destroy
has_many :projects, through: :project_users
belongs_to :group
has_many :users_credits, dependent: :destroy
has_many :credits, through: :users_credits
has_many :training_credits, through: :users_credits, source: :training_credit
has_many :machine_credits, through: :users_credits, source: :machine_credit
has_many :user_tags, dependent: :destroy
has_many :tags, through: :user_tags
accepts_nested_attributes_for :tags, allow_destroy: true
has_many :exports, dependent: :destroy
has_many :imports, dependent: :nullify
has_one :payment_gateway_object, as: :item
has_many :accounting_periods, foreign_key: 'closed_by', dependent: :nullify
# fix for create admin user
before_save do
email&.downcase!
end
before_create :assign_default_role
after_commit :create_gateway_customer, on: [:create]
after_commit :notify_admin_when_user_is_created, on: :create
after_create :init_dependencies
after_update :notify_group_changed, if: :saved_change_to_group_id?
after_update :update_invoicing_profile, if: :invoicing_data_was_modified?
after_update :update_statistic_profile, if: :statistic_data_was_modified?
before_destroy :remove_orphan_drafts
attr_accessor :cgu
delegate :first_name, to: :profile
delegate :last_name, to: :profile
delegate :subscriptions, to: :statistic_profile
delegate :reservations, to: :statistic_profile
delegate :trainings, to: :statistic_profile
delegate :my_projects, to: :statistic_profile
delegate :prepaid_packs, to: :statistic_profile
delegate :wallet, to: :invoicing_profile
delegate :wallet_transactions, to: :invoicing_profile
delegate :invoices, to: :invoicing_profile
validate :cgu_must_accept, if: :new_record?
validates :username, presence: true, uniqueness: true, length: { maximum: 30 }
scope :active, -> { where(is_active: true) }
scope :without_subscription, -> { includes(statistic_profile: [:subscriptions]).where(subscriptions: { statistic_profile_id: nil }) }
scope :with_subscription, -> { joins(statistic_profile: [:subscriptions]) }
scope :not_confirmed, -> { where(confirmed_at: nil) }
scope :inactive_for_3_years, -> { where('users.last_sign_in_at < ?', 3.years.ago) }
def to_json(*)
ApplicationController.new.view_context.render(
partial: 'api/members/member',
locals: { member: self },
formats: [:json],
handlers: [:jbuilder]
)
end
def self.admins
User.with_role(:admin)
end
def self.members
User.with_role(:member)
end
def self.partners
User.with_role(:partner)
end
def self.managers
User.with_role(:manager)
end
def self.admins_and_managers
User.with_any_role(:admin, :manager)
end
def self.online_payers
User.with_any_role(:manager, :member)
end
def self.adminsys
return unless Rails.application.secrets.adminsys_email.present?
User.find_by('lower(email) = ?', Rails.application.secrets.adminsys_email&.downcase)
end
def training_machine?(machine)
return true if admin? || manager?
trainings.map(&:machines).flatten.uniq.include?(machine)
end
def packs?(item)
return true if admin?
PrepaidPackService.user_packs(self, item).count.positive?
end
def next_training_reservation_by_machine(machine)
reservations.where(reservable_type: 'Training', reservable_id: machine.trainings.map(&:id))
.includes(:slots)
.where('slots.start_at>= ?', DateTime.current)
.order('slots.start_at': :asc)
.references(:slots)
.limit(1)
.first
end
def subscribed_plan
return nil if subscription.nil? || subscription.expired_at < DateTime.current
subscription.plan
end
def subscription
subscriptions.order(:created_at).last
end
def admin?
has_role? :admin
end
def member?
has_role? :member
end
def manager?
has_role? :manager
end
def partner?
has_role? :partner
end
def privileged?
admin? || manager?
end
def role
if admin?
'admin'
elsif manager?
'manager'
elsif member?
'member'
else
'other'
end
end
def all_projects
my_projects.to_a.concat projects
end
def generate_subscription_invoice(operator_profile_id)
return unless subscription
subscription.generate_and_save_invoice(operator_profile_id)
end
def active_for_authentication?
super && is_active?
end
def self.from_omniauth(auth)
logger = SsoLogger.new
logger.debug "[User::from_omniauth] initiated with parameter #{auth}"
active_provider = AuthProvider.active
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|
# execute this regardless of whether record exists or not (-> User#tap)
# this will init or update the user thanks to the information retrieved from the SSO
logger.debug user.id.nil? ? 'no user found, creating a new one' : "found user id=#{user.id}"
user.profile ||= Profile.new
auth.info.mapping.each do |key, value|
logger.debug "mapping info #{key} with value=#{value}"
user.set_data_from_sso_mapping(key, value)
end
logger.debug "generating a new password"
user.password = Devise.friendly_token[0, 20]
end
end
def need_completion?
statistic_profile.gender.nil? || profile.first_name.blank? || profile.last_name.blank? || username.blank? ||
email.blank? || encrypted_password.blank? || group_id.nil? || statistic_profile.birthday.blank? ||
(Setting.get('phone_required') && profile.phone.blank?) ||
(Setting.get('address_required') && invoicing_profile.address&.address&.blank?)
end
## Retrieve the requested data in the User and user's Profile tables
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
def get_data_from_sso_mapping(sso_mapping)
parsed = /^(user|profile)\.(.+)$/.match(sso_mapping)
if parsed[1] == 'user'
self[parsed[2].to_sym]
elsif parsed[1] == 'profile'
case sso_mapping
when 'profile.avatar'
profile.user_avatar.remote_attachment_url
when 'profile.address'
invoicing_profile.address.address
when 'profile.organization_name'
invoicing_profile.organization.name
when 'profile.organization_address'
invoicing_profile.organization.address.address
when 'profile.gender'
statistic_profile.gender
when 'profile.birthday'
statistic_profile.birthday
else
profile[parsed[2].to_sym]
end
end
end
## Set some data on the current user, according to the sso_key given
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
## @param data {*} the data to put in the given key. Eg. 'user@example.com'
def set_data_from_sso_mapping(sso_mapping, data)
if sso_mapping.to_s.start_with? 'user.'
self[sso_mapping[5..-1].to_sym] = data unless data.nil?
elsif sso_mapping.to_s.start_with? 'profile.'
case sso_mapping.to_s
when 'profile.avatar'
profile.user_avatar ||= UserAvatar.new
profile.user_avatar.remote_attachment_url = data
when 'profile.address'
invoicing_profile ||= InvoicingProfile.new
invoicing_profile.address ||= Address.new
invoicing_profile.address.address = data
when 'profile.organization_name'
invoicing_profile ||= InvoicingProfile.new
invoicing_profile.organization ||= Organization.new
invoicing_profile.organization.name = data
when 'profile.organization_address'
invoicing_profile ||= InvoicingProfile.new
invoicing_profile.organization ||= Organization.new
invoicing_profile.organization.address ||= Address.new
invoicing_profile.organization.address.address = data
when 'profile.gender'
statistic_profile ||= StatisticProfile.new
statistic_profile.gender = data
when 'profile.birthday'
statistic_profile ||= StatisticProfile.new
statistic_profile.birthday = data
else
profile[sso_mapping[8..-1].to_sym] = data unless data.nil?
end
end
end
## used to allow the migration of existing users between authentication providers
def generate_auth_migration_token
update_attributes(auth_token: Devise.friendly_token)
end
## link the current user to the given provider (omniauth attributes hash)
## and remove the auth_token to mark his account as "migrated"
def link_with_omniauth_provider(auth)
active_provider = AuthProvider.active
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
if User.where(provider: auth.provider, uid: auth.uid).size.positive?
raise DuplicateIndexError, "This #{active_provider.name} account is already linked to an existing user"
end
update_attributes(provider: auth.provider, uid: auth.uid, auth_token: nil)
end
## Merge the provided User's SSO details into the current user and drop the provided user to ensure the unity
## @param sso_user {User} the provided user will be DELETED after the merge was successful
def merge_from_sso(sso_user)
logger = SsoLogger.new
logger.debug "[User::merge_from_sso] initiated with parameter #{sso_user}"
# update the attributes to link the account to the sso account
self.provider = sso_user.provider
self.uid = sso_user.uid
# remove the token
self.auth_token = nil
self.merged_at = DateTime.current
# check that the email duplication was resolved
if sso_user.email.end_with? '-duplicate'
email_addr = sso_user.email.match(/^<([^>]+)>.{20}-duplicate$/)[1]
logger.error 'duplicate email was not resolved'
raise(DuplicateIndexError, email_addr) unless email_addr == email
end
# update the user's profile to set the data managed by the SSO
auth_provider = AuthProvider.from_strategy_name(sso_user.provider)
logger.debug "found auth_provider=#{auth_provider.name}"
auth_provider.sso_fields.each do |field|
value = sso_user.get_data_from_sso_mapping(field)
logger.debug "mapping sso field #{field} with value=#{value}"
# we do not merge the email field if its end with the special value '-duplicate' as this means
# that the user is currently merging with the account that have the same email than the sso
set_data_from_sso_mapping(field, value) unless field == 'user.email' && value.end_with?('-duplicate')
end
# run the account transfer in an SQL transaction to ensure data integrity
begin
User.transaction do
# remove the temporary account
logger.debug 'removing the temporary user'
sso_user.destroy
# finally, save the new details
logger.debug 'saving the updated user'
save!
end
rescue ActiveRecord::RecordInvalid => e
logger.error "error while merging user #{sso_user.id} into #{id}: #{e.message}"
raise e
end
end
def self.mapping
# we protect some fields as they are designed to be managed by the system and must not be updated externally
blacklist = %w[id encrypted_password reset_password_token reset_password_sent_at remember_created_at
sign_in_count current_sign_in_at last_sign_in_at current_sign_in_ip last_sign_in_ip confirmation_token
confirmed_at confirmation_sent_at unconfirmed_email failed_attempts unlock_token locked_at created_at
updated_at slug provider auth_token merged_at]
User.columns_hash
.map { |k, v| [k, v.type.to_s] }
.delete_if { |col| blacklist.include?(col[0]) }
end
# will update the statistic_profile after a group switch or a role update
def update_statistic_profile
raise NoProfileError if statistic_profile.nil? || statistic_profile.id.nil?
statistic_profile.update_attributes(
group_id: group_id,
role_id: roles.first.id
)
end
def organization?
!invoicing_profile.organization.nil?
end
protected
# remove projects drafts that are not linked to another user
def remove_orphan_drafts
orphans = my_projects
.joins('LEFT JOIN project_users ON projects.id = project_users.project_id')
.where('project_users.project_id IS NULL')
.where(state: 'draft')
orphans.map(&:destroy!)
end
def confirmation_required?
Setting.get('confirmation_required') ? super : false
end
private
def assign_default_role
add_role(:member) if roles.blank?
end
def cached_has_role?(role)
roles = Rails.cache.fetch(
roles_for: { object_id: object_id },
expires_in: 1.day,
race_condition_ttl: 2.seconds
) { roles.map(&:name) }
roles.include?(role.to_s)
end
def cgu_must_accept
errors.add(:cgu, I18n.t('activerecord.errors.messages.empty')) if cgu == '0'
end
def create_gateway_customer
PaymentGatewayService.new.create_user(id)
end
def send_devise_notification(notification, *args)
devise_mailer.send(notification, self, *args).deliver_later
end
def notify_admin_when_user_is_created
if need_completion? && !provider.nil?
NotificationCenter.call type: 'notify_admin_when_user_is_imported',
receiver: User.admins,
attached_object: self
else
NotificationCenter.call type: 'notify_admin_when_user_is_created',
receiver: User.admins_and_managers,
attached_object: self
end
end
def notify_group_changed
return unless changes[:group_id]&.first
ex_group = Group.find(changes[:group_id].first)
meta_data = { ex_group_name: ex_group.name }
NotificationCenter.call type: :notify_admin_user_group_changed,
receiver: User.admins_and_managers,
attached_object: self,
meta_data: meta_data
NotificationCenter.call type: :notify_user_user_group_changed,
receiver: self,
attached_object: self
end
def invoicing_data_was_modified?
saved_change_to_email?
end
def statistic_data_was_modified?
saved_change_to_group_id?
end
def init_dependencies
if invoicing_profile.nil?
ip = InvoicingProfile.create!(
user: self,
email: email,
first_name: first_name,
last_name: last_name
)
else
update_invoicing_profile
end
if wallet.nil?
ip ||= invoicing_profile
Wallet.create!(
invoicing_profile: ip
)
end
if statistic_profile.nil?
StatisticProfile.create!(
user: self,
group_id: group_id,
role_id: roles.first.id
)
else
update_statistic_profile
end
end
def update_invoicing_profile
raise NoProfileError if invoicing_profile.nil?
invoicing_profile.update_attributes(
email: email,
first_name: first_name,
last_name: last_name
)
end
end