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.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/webhookdb/customer.rb', line 80 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
66 67 68 69 70 71 72 73 74 |
# File 'lib/webhookdb/customer.rb', line 66 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.
131 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 |
# File 'lib/webhookdb/customer.rb', line 131 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>.
98 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 |
# File 'lib/webhookdb/customer.rb', line 98 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
62 63 64 |
# File 'lib/webhookdb/customer.rb', line 62 def self.with_email(e) return self.dataset.with_email(e).first end |
Instance Method Details
#add_membership(opts = {}) ⇒ Object
:section: Memberships
211 212 213 214 215 216 217 |
# File 'lib/webhookdb/customer.rb', line 211 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
199 200 201 |
# File 'lib/webhookdb/customer.rb', line 199 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.
255 256 257 258 259 |
# File 'lib/webhookdb/customer.rb', line 255 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
293 294 295 |
# File 'lib/webhookdb/customer.rb', line 293 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.
298 299 300 301 302 |
# File 'lib/webhookdb/customer.rb', line 298 def before_soft_delete self.email = "#{Time.now.to_f}+#{self[:email]}" self.password = "aA1!#{SecureRandom.hex(8)}" super end |
#default_organization ⇒ Object
223 224 225 |
# File 'lib/webhookdb/customer.rb', line 223 def default_organization return self.default_membership&.organization end |
#encrypted_password ⇒ Object
Fetch the user’s password as an BCrypt::Password object.
238 239 240 241 |
# File 'lib/webhookdb/customer.rb', line 238 def encrypted_password digest = self.password_digest or return nil return BCrypt::Password.new(digest) end |
#ensure_role(role_or_name) ⇒ Object
193 194 195 196 197 |
# File 'lib/webhookdb/customer.rb', line 193 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
203 204 205 |
# File 'lib/webhookdb/customer.rb', line 203 def greeting return self.name.present? ? self.name : "there" end |
#password=(unencrypted) ⇒ Object
Set the password to the given unencrypted
String.
244 245 246 247 248 249 250 251 |
# File 'lib/webhookdb/customer.rb', line 244 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
227 228 229 230 231 |
# File 'lib/webhookdb/customer.rb', line 227 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.
187 188 189 190 191 |
# File 'lib/webhookdb/customer.rb', line 187 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
285 286 287 |
# File 'lib/webhookdb/customer.rb', line 285 def unverified? return !self.email_verified? && !self.phone_verified? end |
#us_phone ⇒ Object
:section: Phone
277 278 279 |
# File 'lib/webhookdb/customer.rb', line 277 def us_phone return Phony.format(self.phone, format: :national) end |
#us_phone=(s) ⇒ Object
281 282 283 |
# File 'lib/webhookdb/customer.rb', line 281 def us_phone=(s) self.phone = Webhookdb::PhoneNumber::US.normalize(s) end |
#validate ⇒ Object
:section: Sequel Validation
311 312 313 314 315 316 317 |
# File 'lib/webhookdb/customer.rb', line 311 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
219 220 221 |
# File 'lib/webhookdb/customer.rb', line 219 def verified_member_of?(org) return !org.verified_memberships_dataset.where(customer_id: self.id).empty? end |