Class: Account
Overview
== Schema Information
Table name: accounts
Database name: primary
id :integer not null, primary key
authentication_mode :integer default(0), not null
authentication_token :string(255)
confirmation_sent_at :datetime
confirmation_token :string(255)
confirmed_at :datetime
current_sign_in_at :datetime
current_sign_in_ip :string(255)
disabled :boolean default(FALSE), not null
email :citext
encrypted_password :string(255) default("")
failed_attempts :integer default(0)
ignore_ip_visit_check :boolean default(FALSE), not null
inherited_role_ids :integer default([]), is an Array
inherited_role_names :string default([]), is an Array
invitation_accepted_at :datetime
invitation_created_at :datetime
invitation_limit :integer
invitation_sent_at :datetime
invitation_token :string(255)
invited_by_type :string(255)
is_guest :boolean
last_login :datetime
last_sign_in_at :datetime
last_sign_in_ip :string(255)
locked_at :datetime
login :citext not null
name :string(100) default("")
password_salt :string(255) default("")
remember_created_at :datetime
require_myp_migration :boolean default(FALSE)
reset_password_sent_at :datetime
reset_password_token :string(255)
return_path_for_invite :string(255)
sign_in_count :integer default(0)
unlock_token :string(255)
created_at :datetime
updated_at :datetime
invited_by_id :integer
my_projects_user_id :integer
party_id :integer
Indexes
index_accounts_on_authentication_token (authentication_token) UNIQUE
index_accounts_on_confirmation_token (confirmation_token) UNIQUE
index_accounts_on_email (email)
index_accounts_on_inherited_role_names (inherited_role_names) USING gin
index_accounts_on_invitation_token (invitation_token)
index_accounts_on_invited_by_id (invited_by_id)
index_accounts_on_login (login) UNIQUE
index_accounts_on_my_projects_user_id (my_projects_user_id)
index_accounts_on_party_id (party_id)
index_accounts_on_reset_password_token (reset_password_token) UNIQUE
index_accounts_on_unlock_token (unlock_token) UNIQUE
Foreign Keys
accounts_party_id_fkey (party_id => parties.id)
Defined Under Namespace
Classes: Inviter
Constant Summary
collapse
- AUTH_INTERNAL =
0
- AUTH_GOOGLE =
1
Models::Auditable::ALWAYS_IGNORED
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Delegated Instance Attributes
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
#publish_event
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_id, *arguments) ⇒ Object
472
473
474
475
476
477
478
479
480
481
482
|
# File 'app/models/account.rb', line 472
def method_missing(method_id, *arguments, &)
if /^is_\w+?/.match?(method_id.to_s)
self.class.send :define_method, method_id do
role_to_check = method_id.to_s.match(/^is_(\w+)?/)[1]
send(:has_role?, role_to_check)
end
send(method_id)
else
super
end
end
|
Instance Attribute Details
#authentication_mode ⇒ Object
106
|
# File 'app/models/account.rb', line 106
validates :authentication_mode, presence: true, inclusion: { in: [AUTH_INTERNAL, AUTH_GOOGLE] }
|
#email ⇒ Object
108
|
# File 'app/models/account.rb', line 108
validates :email, presence: true, email_format: true
|
#email_reset_instructions ⇒ Object
Returns the value of attribute email_reset_instructions.
121
122
123
|
# File 'app/models/account.rb', line 121
def email_reset_instructions
@email_reset_instructions
end
|
#invitation_code ⇒ Object
Returns the value of attribute invitation_code.
121
122
123
|
# File 'app/models/account.rb', line 121
def invitation_code
@invitation_code
end
|
#login ⇒ Object
107
|
# File 'app/models/account.rb', line 107
validates :login, presence: true, uniqueness: true
|
#marketing_sign_up ⇒ Object
Returns the value of attribute marketing_sign_up.
121
122
123
|
# File 'app/models/account.rb', line 121
def marketing_sign_up
@marketing_sign_up
end
|
#password ⇒ Object
from devise validatable, we only keep password validations, not email since they require uniqueness if present, which we do not
Validations:
-
Presence
({ if: :password_required? })
-
Confirmation
({ if: -> { password.present? } })
-
Length
({ within: Devise.password_length, allow_blank: true })
112
|
# File 'app/models/account.rb', line 112
validates :password, presence: { if: :password_required? }
|
#password_confirmation ⇒ Object
118
|
# File 'app/models/account.rb', line 118
validates :password_confirmation, presence: true, if: -> { password.present? }
|
#require_password ⇒ Object
Returns the value of attribute require_password.
121
122
123
|
# File 'app/models/account.rb', line 121
def require_password
@require_password
end
|
#skip_notification ⇒ Object
Returns the value of attribute skip_notification.
121
122
123
|
# File 'app/models/account.rb', line 121
def skip_notification
@skip_notification
end
|
Class Method Details
.account_api_signed_in(api_authentication_token) ⇒ Object
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
# File 'app/models/account.rb', line 144
def self.account_api_signed_in(api_authentication_token)
res = nil
api_auth = ApiAuthentication.find_by(api_authentication_token: api_authentication_token)
if api_auth
if api_auth.expired?
api_auth.destroy
res = nil
else
res = api_auth.account
end
end
res
end
|
.active_accounts ⇒ ActiveRecord::Relation<Account>
A relation of Accounts that are active accounts. Active Record Scope
100
|
# File 'app/models/account.rb', line 100
scope :active_accounts, -> { where.not(disabled: true) }
|
.email_logins ⇒ ActiveRecord::Relation<Account>
A relation of Accounts that are email logins. Active Record Scope
101
|
# File 'app/models/account.rb', line 101
scope :email_logins, -> { where("accounts.login LIKE '%@%'") }
|
.get_unique_login_for_email(email) ⇒ Object
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
# File 'app/models/account.rb', line 159
def self.get_unique_login_for_email(email)
login = email
if Account.where(login: email).exists?
name_part, = email.split('@')
base_login = name_part
counter = 0
login = base_login
while Account.where(login: login).exists?
counter += 1
login = "#{base_login}-#{counter}"
end
end
login
end
|
.new_with_session(params, session) ⇒ Object
138
139
140
141
142
|
# File 'app/models/account.rb', line 138
def self.new_with_session(params, session)
super.tap do |account|
account.login ||= session.dig('devise.facebook_data', 'extra', 'raw_info', 'email')
end
end
|
Instance Method Details
#ability ⇒ Object
195
196
197
|
# File 'app/models/account.rb', line 195
def ability
@ability ||= Ability.new(party)
end
|
#account_created_notify_reps_and_master_account ⇒ Object
410
411
412
413
414
|
# File 'app/models/account.rb', line 410
def account_created_notify_reps_and_master_account
return if party&.is_employee? || skip_notification
notify(activity: 'created_account') if party.present?
end
|
#active_for_authentication? ⇒ Boolean
178
179
180
181
|
# File 'app/models/account.rb', line 178
def active_for_authentication?
Rails.logger.debug { "active_for_authentication? disabled?: #{disabled?}" }
super && !disabled?
end
|
#api_authentications ⇒ ActiveRecord::Relation<ApiAuthentication>
96
|
# File 'app/models/account.rb', line 96
has_many :api_authentications, dependent: :destroy
|
#api_sign_in!(is_guest = false) ⇒ Object
Here we manage api_authentications which is used only for API authentication, we don't want it to mix or reset the usual web based authentication token which is used for employee 'login as this customer" login or "continue as guest" accounts for email link login and is reset on successful registration or social login authentication. We want API authentication - the only kind of api login mechanism via email/password, social login or continue as guest via a non web based app - to be independent
317
318
319
320
321
322
323
|
# File 'app/models/account.rb', line 317
def api_sign_in!(is_guest = false)
api_auth = api_authentications.build
api_auth.is_guest = is_guest
save!
api_auth.api_authentication_token
end
|
#api_sign_out!(api_authentication_token) ⇒ Object
325
326
327
328
329
|
# File 'app/models/account.rb', line 325
def api_sign_out!(api_authentication_token)
api_auth = api_authentications.find_by(api_authentication_token: api_authentication_token)
api_auth&.destroy
end
|
#append_token(path_or_url, persist = nil) ⇒ Object
378
379
380
381
382
383
384
385
386
387
388
|
# File 'app/models/account.rb', line 378
def append_token(path_or_url, persist = nil)
restore_authentication_token!
uri = Addressable::URI.parse(path_or_url)
new_query_ar = uri.query ? Addressable::URI.form_unencode(uri.query) : []
new_query_ar << ['auth_token', authentication_token]
new_query_ar << ['account_login', login]
new_query_ar << ['persist', persist] if persist
new_query_ar << ['quar_legacy_dt_params', 1]
uri.query = Addressable::URI.form_encode(new_query_ar)
uri.to_s
end
|
#apply_omniauth(omniauth) ⇒ Object
If you use a social media account login, then this extracts the name and stores it in the party
and builds the authentication
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
|
# File 'app/models/account.rb', line 268
def apply_omniauth(omniauth)
logger.debug "apply_omniauth(omniauth): party.id: #{party.id}"
logger.debug "apply_omniauth(omniauth): email: #{email}"
logger.debug "apply_omniauth(omniauth): omniauth: #{omniauth.inspect}"
if login.blank?
self.login = Authentication.(omniauth)
self.email = Authentication.(omniauth)
logger.debug "apply_omniauth(omniauth): email: #{email}"
logger.debug "apply_omniauth(omniauth): party.email: #{begin
party.email
rescue StandardError
'n/a'
end}"
end
if name.blank? || name.downcase.index('guest').present?
omniauth_name = Authentication.(omniauth)
logger.debug "apply_omniauth(omniauth): omniauth_name: #{omniauth_name}"
self.name = omniauth_name if omniauth_name
logger.debug "apply_omniauth(omniauth): SET name: #{name}"
logger.debug "apply_omniauth(omniauth): party.name: #{begin
party.name
rescue StandardError
'n/a'
end}"
end
authentications.build(provider: omniauth['provider'], uid: omniauth['uid'])
end
|
#auth_token_required? ⇒ Boolean
373
374
375
|
# File 'app/models/account.rb', line 373
def auth_token_required?
login.present? && authentications.empty? && encrypted_password.blank? && !is_employee?
end
|
#authentication_internal? ⇒ Boolean
199
200
201
|
# File 'app/models/account.rb', line 199
def authentication_internal?
authentication_mode == AUTH_INTERNAL
end
|
#authentication_methods ⇒ Object
305
306
307
308
309
310
311
312
313
|
# File 'app/models/account.rb', line 305
def authentication_methods
auth_methods = []
auth_methods << 'password' if encrypted_password.present?
authentications.each do |auth|
auth_methods << auth.provider
end
auth_methods
end
|
#authentication_mode_name ⇒ Object
235
236
237
|
# File 'app/models/account.rb', line 235
def authentication_mode_name
%w[Internal Google][authentication_mode]
end
|
#authentications ⇒ ActiveRecord::Relation<Authentication>
95
|
# File 'app/models/account.rb', line 95
has_many :authentications, dependent: :destroy
|
#can? ⇒ Object
134
|
# File 'app/models/account.rb', line 134
delegate :can?, :cannot?, to: :ability
|
#can_access_mcp? ⇒ Boolean
MCP (Model Context Protocol) access methods
Used for AI assistant integrations like Cursor/Claude
334
335
336
|
# File 'app/models/account.rb', line 334
def can_access_mcp?
is_employee? && has_role?('mcp_access')
end
|
#can_impersonate?(customer_account) ⇒ Boolean
221
222
223
224
225
226
|
# File 'app/models/account.rb', line 221
def can_impersonate?(customer_account)
is_manager? ||
is_customer_service_rep? ||
party_id == customer_account.customer.primary_sales_rep_id ||
party_id == customer_account.customer.secondary_sales_rep_id
end
|
#cannot? ⇒ Object
Alias for Ability#cannot?
134
|
# File 'app/models/account.rb', line 134
delegate :can?, :cannot?, to: :ability
|
#check_for_reset_instructions ⇒ Object
450
451
452
453
454
455
|
# File 'app/models/account.rb', line 450
def check_for_reset_instructions
return unless email_reset_instructions == '1'
self.email_password_reset_instructions = nil
send_reset_password_instructions
end
|
#confirmation_required? ⇒ Boolean
431
432
433
434
435
436
|
# File 'app/models/account.rb', line 431
def confirmation_required?
return false if party&.is_employee?
return false if authentications.present?
true
end
|
#customer ⇒ Object
203
|
# File 'app/models/account.rb', line 203
delegate :customer, to: :party
|
92
|
# File 'app/models/account.rb', line 92
belongs_to :employee, class_name: 'Employee', foreign_key: :party_id, optional: true
|
#ensure_authentication_token ⇒ Object
360
361
362
363
364
365
|
# File 'app/models/account.rb', line 360
def ensure_authentication_token
return if authentication_token.present?
self.authentication_token = generate_authentication_token
save
end
|
#fetch_inherited_role_names ⇒ Object
421
422
423
|
# File 'app/models/account.rb', line 421
def fetch_inherited_role_names
Role.where(id: inherited_role_ids).order(:name).pluck(:name)
end
|
#fully_enabled? ⇒ Boolean
239
240
241
|
# File 'app/models/account.rb', line 239
def fully_enabled?
login.present? && (encrypted_password.present? || authentications.present?)
end
|
#generate_mcp_token! ⇒ Object
338
339
340
341
342
|
# File 'app/models/account.rb', line 338
def generate_mcp_token!
raise 'Account does not have MCP access' unless can_access_mcp?
api_sign_in!
end
|
#has_role?(*roles_in_question, admin_check: true) ⇒ Boolean
228
229
230
231
232
233
|
# File 'app/models/account.rb', line 228
def has_role?(*roles_in_question, admin_check: true)
roles_array = roles_in_question.flatten.map(&:to_s).map(&:downcase)
(admin_check && inherited_role_names.include?('admin')) ||
inherited_role_names.map(&:downcase).intersect?(roles_array)
end
|
248
249
250
251
252
253
254
|
# File 'app/models/account.rb', line 248
def (_action)
if party&.respond_to?(:primary_sales_rep?)
{ from: "'#{party.primary_sales_rep.name}' <#{party.primary_sales_rep.email}>" }
else
{}
end
end
|
#inactive_message ⇒ Object
183
184
185
|
# File 'app/models/account.rb', line 183
def inactive_message
'You account has been disabled, please contact us at (800) 875-5285'
end
|
#invitations ⇒ ActiveRecord::Relation<Invitation>
94
|
# File 'app/models/account.rb', line 94
has_many :invitations, class_name: self.class.to_s, as: :invited_by
|
#is_admin? ⇒ Boolean
213
214
215
|
# File 'app/models/account.rb', line 213
def is_admin?
inherited_role_names.include?('admin')
end
|
#is_customer? ⇒ Boolean
209
210
211
|
# File 'app/models/account.rb', line 209
def is_customer?
party.is_a?(Customer)
end
|
#is_employee? ⇒ Boolean
205
206
207
|
# File 'app/models/account.rb', line 205
def is_employee?
party.is_a?(Employee)
end
|
#is_manager? ⇒ Boolean
217
218
219
|
# File 'app/models/account.rb', line 217
def is_manager?
is_admin? || inherited_role_names.any? { |rn| rn =~ /_manager$/ && rn != 'item_manager' }
end
|
#login_is_email? ⇒ Boolean
174
175
176
|
# File 'app/models/account.rb', line 174
def login_is_email?
login =~ RFC822::EMAIL
end
|
#notify(options) ⇒ Object
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
|
# File 'app/models/account.rb', line 344
def notify(options)
mailers = []
case options[:activity]
when 'update_password'
mailers << AccountMailer.password_changed(id)
when 'update_email'
mailers << AccountMailer.email_changed(id, options[:old_email], options[:old_login], options[:old_email]) if options[:old_email] != email
mailers << AccountMailer.email_changed(id, options[:old_email], options[:old_login], email) if (options[:old_email] != email) || (options[:old_login] != login)
when 'created_account'
mailers << AccountMailer.created_account(id)
end
mailers.each { |m| m.deliver_later(wait: 10.seconds) }
end
|
#obfuscated_email ⇒ Object
425
426
427
|
# File 'app/models/account.rb', line 425
def obfuscated_email
email&.gsub(/(?<=.{2}).*@.*(?=\S{2})/, '****@****')
end
|
#omniauth_provider_icon_basenames ⇒ Object
297
298
299
|
# File 'app/models/account.rb', line 297
def omniauth_provider_icon_basenames
Authentication::PROVIDERS.select { |p, _h| authentications.find_by(provider: p) }.map { |_p, h| h[:icon] }
end
|
91
|
# File 'app/models/account.rb', line 91
belongs_to :party, optional: true
|
#password_required? ⇒ Boolean
256
257
258
259
260
261
262
263
264
|
# File 'app/models/account.rb', line 256
def password_required?
return false if authentication_mode == AUTH_GOOGLE
res = true
res = false if authentications.any?
res = false if encrypted_password.blank?
res = true if require_password
res && (!persisted? || !password.nil? || !password_confirmation.nil?)
end
|
#pending_confirmation? ⇒ Boolean
243
244
245
|
# File 'app/models/account.rb', line 243
def pending_confirmation?
invitation_token.present?
end
|
#push_login_to_email ⇒ Object
416
417
418
419
|
# File 'app/models/account.rb', line 416
def push_login_to_email
self.email = login if email.blank? && login_is_email?
end
|
#respond_to?(method_id, include_private = false) ⇒ Boolean
301
302
303
|
# File 'app/models/account.rb', line 301
def respond_to?(method_id, include_private = false)
/^is_\w+?/.match?(method_id.to_s) || super
end
|
#restore_authentication_token! ⇒ Object
367
368
369
370
371
|
# File 'app/models/account.rb', line 367
def restore_authentication_token!
self.authentication_token = generate_authentication_token
save
super
end
|
#role_inheritance ⇒ Object
461
462
463
464
465
466
467
468
469
470
|
# File 'app/models/account.rb', line 461
def role_inheritance
roles.each do |r|
arids = r.ancestors_ids if (res = (role_ids & arids)).present?
role_names = Role.where(id: res).pluck(:name)
errors.add(:base, "Role #{r.name} is already inherited by role #{role_names.join(', ')} and cannot be assigned")
end
end
end
|
#roles ⇒ ActiveRecord::Relation<Role>
98
|
# File 'app/models/account.rb', line 98
has_and_belongs_to_many :roles, after_add: :touch_me, after_remove: :touch_me, inverse_of: :accounts
|
#set_default_marketing_preferences(locale) ⇒ Object
187
188
189
190
191
192
193
|
# File 'app/models/account.rb', line 187
def set_default_marketing_preferences(locale)
self.marketing_sign_up = if (locale == :'en-CA') || (locale == :'fr-CA')
false
else
true
end
end
|
#set_defaults ⇒ Object
438
439
440
441
|
# File 'app/models/account.rb', line 438
def set_defaults
self.authentication_mode ||= AUTH_INTERNAL
push_login_to_email
end
|
#signed_login_url(target_url, purpose: :magic_login, expires_in: 7.days) ⇒ Object
Build a magic-login URL — the embedded login_token authenticates this
account when the URL is visited. Tokens are purpose-tagged via Rails'
signed_id so a token minted for one flow (e.g. cart recovery) cannot
be replayed against another (e.g. blog preview). TTL bound; no DB column.
Example:
account.signed_login_url(retrieve_my_cart_url(host: WEB_HOSTNAME))
account.signed_login_url(change_password_my_account_url(...), expires_in: 24.hours)
398
399
400
401
402
403
404
405
406
407
408
|
# File 'app/models/account.rb', line 398
def signed_login_url(target_url, purpose: :magic_login, expires_in: 7.days)
uri = Addressable::URI.parse(target_url)
new_query_ar = uri.query ? Addressable::URI.form_unencode(uri.query) : []
new_query_ar.reject! { |key, _| key == 'login_token' }
new_query_ar << ['login_token', signed_id(purpose: purpose, expires_in: expires_in)]
uri.query = Addressable::URI.form_encode(new_query_ar)
uri.to_s
end
|
#timeout_in ⇒ Object
484
485
486
487
|
# File 'app/models/account.rb', line 484
def timeout_in
auth_token_required? ? 30.minutes : Devise.timeout_in
end
|
#touch_me(_role) ⇒ Object
457
458
459
|
# File 'app/models/account.rb', line 457
def touch_me(_role)
touch unless new_record?
end
|
443
444
445
446
447
448
|
# File 'app/models/account.rb', line 443
def update_matching_contact_point_if_needed
return unless email_changed? && valid?
cp = party.contact_points.where(detail: email_was, category: 'email').first
cp&.update_attribute(:detail, email)
end
|