Class: Webhookdb::Customer
- Inherits:
-
Object
- Object
- Webhookdb::Customer
- Extended by:
- MethodUtilities
- Includes:
- Appydays::Configurable
- Defined in:
- lib/webhookdb/customer.rb
Defined Under Namespace
Classes: InvalidPassword, ResetCode, SignupDisabled
Constant Summary collapse
- MIN_PASSWORD_LENGTH =
8
- PLACEHOLDER_PASSWORD_DIGEST =
A bcrypt digest that’s valid, but not a real digest. Used as a placeholder for accounts with no passwords, which makes them impossible to authenticate. Or at least much less likely than with a random string.
"$2a$11$....................................................."
- DELETED_EMAIL_PATTERN =
Regex that matches the prefix of a deleted user’s email
/^(?<prefix>\d+(?:\.\d+)?)\+(?<rest>.*)$/
Class Method Summary collapse
-
.find_or_create_default_organization(customer) ⇒ Array<TrueClass,FalseClass,Webhookdb::OrganizationMembership>
Make sure the customer has a default organization.
- .find_or_create_for_email(email) ⇒ Object
-
.finish_otp(me, token:) ⇒ Object
Tuple of <Step, Customer>.
-
.register_or_login(email:) ⇒ Object
Tuple of <Step, Customer>.
- .with_email(e) ⇒ Object
Instance Method Summary collapse
-
#add_membership(opts = {}) ⇒ Object
:section: Memberships.
- #admin? ⇒ Boolean
-
#authenticate(unencrypted) ⇒ Object
Attempt to authenticate the user with the specified
unencrypted
password. -
#before_create ⇒ Object
:section: Sequel Hooks.
-
#before_soft_delete ⇒ Object
Soft-delete hook – prep the user for deletion.
- #default_organization ⇒ Object
-
#encrypted_password ⇒ Object
Fetch the user’s password as an BCrypt::Password object.
- #ensure_role(role_or_name) ⇒ Object
- #greeting ⇒ Object
-
#password=(unencrypted) ⇒ Object
Set the password to the given
unencrypted
String. - #replace_default_membership(new_mem) ⇒ Object
-
#should_skip_authentication? ⇒ Boolean
If the SKIP_PHONE|EMAIL_VERIFICATION are set, verify the phone/email.
- #unverified? ⇒ Boolean
-
#us_phone ⇒ Object
:section: Phone.
- #us_phone=(s) ⇒ Object
-
#validate ⇒ Object
:section: Sequel Validation.
- #verified_member_of?(org) ⇒ Boolean
Methods included from MethodUtilities
attr_predicate, attr_predicate_accessor, singleton_attr_accessor, singleton_attr_reader, singleton_attr_writer, singleton_method_alias, singleton_predicate_accessor, singleton_predicate_reader
Class Method Details
.find_or_create_default_organization(customer) ⇒ Array<TrueClass,FalseClass,Webhookdb::OrganizationMembership>
Make sure the customer has a default organization. New registrants, or users who have been invited (so have an existing customer and invited org) get an org created. Default orgs must already be verified as per a DB constraint.
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/webhookdb/customer.rb', line 81 def self.find_or_create_default_organization(customer) mem = customer.default_membership return [false, mem] if mem email = customer.email # We could have no default, but already be in an organization, like if the default was deleted. mem = customer.verified_memberships.first return [false, mem] if mem # We have no verified orgs, so create one. # TODO: this will fail if not unique. We need to make sure we pick a unique name/key. self_org = Webhookdb::Organization.create(name: "#{email} Org", billing_email: email.to_s) mem = customer.add_membership( organization: self_org, membership_role: Webhookdb::Role.admin_role, verified: true, is_default: true, ) self_org.publish_deferred("syncdemodata", self_org.id) if Webhookdb::DemoMode.example_datasets_enabled return [true, mem] end |
.find_or_create_for_email(email) ⇒ Object
67 68 69 70 71 72 73 74 75 |
# File 'lib/webhookdb/customer.rb', line 67 def self.find_or_create_for_email(email) email = email.strip.downcase # If there is no Customer object associated with the email, create one me = Webhookdb::Customer[email:] return [false, me] if me signup_allowed = self.signup_email_allowlist.any? { |pattern| File.fnmatch(pattern, email) } raise SignupDisabled unless signup_allowed return [true, Webhookdb::Customer.create(email:, password: SecureRandom.hex(32))] end |
.finish_otp(me, token:) ⇒ Object
Returns Tuple of <Step, Customer>. Customer is nil if token was invalid.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/webhookdb/customer.rb', line 132 def self.finish_otp(me, token:) if me.nil? step = Webhookdb::Replicator::StateMachineStep.new step.output = %(Sorry, no one with that email exists. Try running: webhookdb auth login [email] ) step.needs_input = false step.complete = true step.error_code = "email_not_exist" return [step, nil] end unless me.should_skip_authentication? begin Webhookdb::Customer::ResetCode.use_code_with_token(token) do |code| raise Webhookdb::Customer::ResetCode::Unusable unless code.customer === me code.customer.save_changes me.refresh end rescue Webhookdb::Customer::ResetCode::Unusable step = Webhookdb::Replicator::StateMachineStep.new step.output = %(Sorry, that token is invalid. Please try again. If you have not gotten a code, use Ctrl+C to close this prompt and request a new code: webhookdb auth login #{me.email} ) step.error_code = "invalid_otp" step.prompt_is_secret = true step.prompt = "Enter the token from your email:" step.needs_input = true step.post_to_url = "/v1/auth" step.post_params = {email: me.email} step.post_params_value_key = "token" return [step, nil] end end welcome_tutorial = "Quick tip: Use `webhookdb services list` to see what services are available." if me.invited_memberships.present? welcome_tutorial = "You have the following pending invites:\n\n" + me.invited_memberships.map { |om| " #{om.organization.display_string}: #{om.invitation_code}" }.join("\n") + "\n\nUse `webhookdb org join [code]` to accept an invitation." end step = Webhookdb::Replicator::StateMachineStep.new step.output = %(Welcome! For help getting started, please check out our docs at https://docs.webhookdb.com. #{welcome_tutorial}) step.needs_input = false step.complete = true return [step, me] end |
.register_or_login(email:) ⇒ Object
Returns Tuple of <Step, Customer>.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/webhookdb/customer.rb', line 99 def self.register_or_login(email:) self.db.transaction do customer_created, me = self.find_or_create_for_email(email) org_created, _membership = self.find_or_create_default_organization(me) me.reset_codes_dataset.usable.each(&:expire!) me.add_reset_code(transport: "email") step = Webhookdb::Replicator::StateMachineStep.new step.output = if customer_created || org_created %(To finish registering, please look for an email we just sent to #{email}. It contains a One Time Password code to validate your email. ) else %(Hello again! To finish logging in, please look for an email we just sent to #{email}. It contains a One Time Password used to log in. ) end step.output += %(You can enter it here, or if you want to finish up from a new prompt, use: webhookdb auth login --username=#{email} --token=<#{Webhookdb::Customer::ResetCode::TOKEN_LENGTH} digit token> ) step.prompt = "Enter the token from your email:" step.prompt_is_secret = true step.needs_input = true step.post_to_url = "/v1/auth" step.post_params = {email:} step.post_params_value_key = "token" return [step, me] end end |
.with_email(e) ⇒ Object
63 64 65 |
# File 'lib/webhookdb/customer.rb', line 63 def self.with_email(e) return self.dataset.with_email(e).first end |
Instance Method Details
#add_membership(opts = {}) ⇒ Object
:section: Memberships
212 213 214 215 216 217 218 |
# File 'lib/webhookdb/customer.rb', line 212 def add_membership(opts={}) if !opts.is_a?(Webhookdb::OrganizationMembership) && !opts.key?(:verified) raise ArgumentError, "must pass :verified or a model into add_membership, it is ambiguous otherwise" end self.associations.delete(opts[:verified] ? :verified_memberships : :invited_memberships) return self.add_all_membership(opts) end |
#admin? ⇒ Boolean
200 201 202 |
# File 'lib/webhookdb/customer.rb', line 200 def admin? return self.roles.include?(Webhookdb::Role.admin_role) end |
#authenticate(unencrypted) ⇒ Object
Attempt to authenticate the user with the specified unencrypted
password. Returns true
if the password matched.
256 257 258 259 260 |
# File 'lib/webhookdb/customer.rb', line 256 def authenticate(unencrypted) return false unless unencrypted return false if self.soft_deleted? return self.encrypted_password == unencrypted end |
#before_create ⇒ Object
:section: Sequel Hooks
294 295 296 |
# File 'lib/webhookdb/customer.rb', line 294 def before_create self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("cus") end |
#before_soft_delete ⇒ Object
Soft-delete hook – prep the user for deletion.
299 300 301 302 303 |
# File 'lib/webhookdb/customer.rb', line 299 def before_soft_delete self.email = "#{Time.now.to_f}+#{self[:email]}" self.password = "aA1!#{SecureRandom.hex(8)}" super end |
#default_organization ⇒ Object
224 225 226 |
# File 'lib/webhookdb/customer.rb', line 224 def default_organization return self.default_membership&.organization end |
#encrypted_password ⇒ Object
Fetch the user’s password as an BCrypt::Password object.
239 240 241 242 |
# File 'lib/webhookdb/customer.rb', line 239 def encrypted_password digest = self.password_digest or return nil return BCrypt::Password.new(digest) end |
#ensure_role(role_or_name) ⇒ Object
194 195 196 197 198 |
# File 'lib/webhookdb/customer.rb', line 194 def ensure_role(role_or_name) role = role_or_name.is_a?(Webhookdb::Role) ? role_or_name : Webhookdb::Role[name: role_or_name] raise "No role for #{role_or_name}" unless role.present? self.add_role(role) unless self.roles_dataset[role.id] end |
#greeting ⇒ Object
204 205 206 |
# File 'lib/webhookdb/customer.rb', line 204 def greeting return self.name.present? ? self.name : "there" end |
#password=(unencrypted) ⇒ Object
Set the password to the given unencrypted
String.
245 246 247 248 249 250 251 252 |
# File 'lib/webhookdb/customer.rb', line 245 def password=(unencrypted) if unencrypted self.check_password_complexity(unencrypted) self.password_digest = BCrypt::Password.create(unencrypted, cost: self.class.password_hash_cost) else self.password_digest = BCrypt::Password.new(PLACEHOLDER_PASSWORD_DIGEST) end end |
#replace_default_membership(new_mem) ⇒ Object
228 229 230 231 232 |
# File 'lib/webhookdb/customer.rb', line 228 def replace_default_membership(new_mem) self.verified_memberships_dataset.update(is_default: false) self.associations.delete(:verified_memberships) new_mem.update(is_default: true) end |
#should_skip_authentication? ⇒ Boolean
If the SKIP_PHONE|EMAIL_VERIFICATION are set, verify the phone/email. Also verify phone and email if the customer email matches the allowlist.
188 189 190 191 192 |
# File 'lib/webhookdb/customer.rb', line 188 def should_skip_authentication? return true if self.class.skip_authentication return true if self.class.skip_authentication_allowlist.any? { |pattern| File.fnmatch(pattern, self.email) } return false end |
#unverified? ⇒ Boolean
286 287 288 |
# File 'lib/webhookdb/customer.rb', line 286 def unverified? return !self.email_verified? && !self.phone_verified? end |
#us_phone ⇒ Object
:section: Phone
278 279 280 |
# File 'lib/webhookdb/customer.rb', line 278 def us_phone return Phony.format(self.phone, format: :national) end |
#us_phone=(s) ⇒ Object
282 283 284 |
# File 'lib/webhookdb/customer.rb', line 282 def us_phone=(s) self.phone = Webhookdb::PhoneNumber::US.normalize(s) end |
#validate ⇒ Object
:section: Sequel Validation
312 313 314 315 316 317 318 |
# File 'lib/webhookdb/customer.rb', line 312 def validate super self.validates_presence(:email) self.validates_format(/[[:graph:]]+@[[:graph:]]+\.[a-zA-Z]{2,}/, :email) self.validates_unique(:email) self.validates_operator(:==, self.email&.downcase&.strip, :email) end |
#verified_member_of?(org) ⇒ Boolean
220 221 222 |
# File 'lib/webhookdb/customer.rb', line 220 def verified_member_of?(org) return !org.verified_memberships_dataset.where(customer_id: self.id).empty? end |