searls-auth 0.2.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +162 -0
  4. data/app/controllers/searls/auth/base_controller.rb +42 -21
  5. data/app/controllers/searls/auth/email_verifications_controller.rb +57 -0
  6. data/app/controllers/searls/auth/logins_controller.rb +60 -39
  7. data/app/controllers/searls/auth/registrations_controller.rb +84 -32
  8. data/app/controllers/searls/auth/requests_password_resets_controller.rb +55 -0
  9. data/app/controllers/searls/auth/resets_passwords_controller.rb +73 -0
  10. data/app/controllers/searls/auth/settings_controller.rb +83 -0
  11. data/app/controllers/searls/auth/verifications_controller.rb +31 -61
  12. data/app/helpers/searls/auth/application_helper.rb +9 -5
  13. data/app/mailers/searls/auth/base_mailer.rb +1 -1
  14. data/app/mailers/searls/auth/email_verification_mailer.rb +29 -0
  15. data/app/mailers/searls/auth/login_link_mailer.rb +3 -3
  16. data/app/mailers/searls/auth/password_reset_mailer.rb +29 -0
  17. data/app/views/searls/auth/email_verification_mailer/verification_email.html.erb +23 -0
  18. data/app/views/searls/auth/email_verification_mailer/verification_email.text.erb +6 -0
  19. data/app/views/searls/auth/login_link_mailer/login_link.html.erb +5 -5
  20. data/app/views/searls/auth/login_link_mailer/login_link.text.erb +4 -5
  21. data/app/views/searls/auth/logins/show.html.erb +12 -4
  22. data/app/views/searls/auth/password_reset_mailer/password_reset.html.erb +23 -0
  23. data/app/views/searls/auth/password_reset_mailer/password_reset.text.erb +6 -0
  24. data/app/views/searls/auth/registrations/pending_email_verification.html.erb +12 -0
  25. data/app/views/searls/auth/registrations/show.html.erb +1 -2
  26. data/app/views/searls/auth/requests_password_resets/show.html.erb +17 -0
  27. data/app/views/searls/auth/resets_passwords/show.html.erb +26 -0
  28. data/app/views/searls/auth/settings/edit.html.erb +31 -0
  29. data/app/views/searls/auth/shared/_login_fields.html.erb +11 -0
  30. data/app/views/searls/auth/shared/_register_fields.html.erb +15 -0
  31. data/config/routes.rb +11 -0
  32. data/lib/searls/auth/authenticates_user.rb +54 -10
  33. data/lib/searls/auth/builds_target_redirect_url.rb +72 -0
  34. data/lib/searls/auth/config.rb +259 -12
  35. data/lib/searls/auth/creates_user.rb +12 -4
  36. data/lib/searls/auth/delivers_password_reset.rb +18 -0
  37. data/lib/searls/auth/emails_link.rb +2 -2
  38. data/lib/searls/auth/emails_verification.rb +33 -0
  39. data/lib/searls/auth/parses_time_safely.rb +32 -0
  40. data/lib/searls/auth/railtie.rb +0 -1
  41. data/lib/searls/auth/resets_password.rb +41 -0
  42. data/lib/searls/auth/updates_settings.rb +149 -0
  43. data/lib/searls/auth/version.rb +1 -1
  44. data/lib/searls/auth.rb +62 -13
  45. metadata +23 -1
@@ -0,0 +1,32 @@
1
+ require "active_support/time"
2
+
3
+ module Searls
4
+ module Auth
5
+ class ParsesTimeSafely
6
+ def parse(input)
7
+ return nil if input.nil?
8
+
9
+ case input
10
+ when String
11
+ parse_string(input)
12
+ when Integer, Float
13
+ Time.at(input).in_time_zone
14
+ else
15
+ if input.respond_to?(:in_time_zone)
16
+ input.in_time_zone
17
+ end
18
+ end
19
+ rescue ArgumentError, TypeError, NoMethodError
20
+ nil
21
+ end
22
+
23
+ private
24
+
25
+ def parse_string(s)
26
+ if !(stripped = s.strip).empty?
27
+ Time.zone.parse(stripped)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -13,7 +13,6 @@ module Searls
13
13
 
14
14
  # Initialize configuration defaults
15
15
  initializer "searls.auth.configure" do |app|
16
- # Set up any configuration defaults here
17
16
  end
18
17
  end
19
18
  end
@@ -0,0 +1,41 @@
1
+ module Searls
2
+ module Auth
3
+ class ResetsPassword
4
+ Result = Struct.new(:success?, :errors, :user, keyword_init: true)
5
+
6
+ def reset(user:, password:, password_confirmation:)
7
+ configuration = Searls::Auth.config
8
+
9
+ if password.blank?
10
+ message = configuration.resolve(:flash_error_after_password_reset_password_blank, {})
11
+ return Result.new(success?: false, errors: Array(message), user: user)
12
+ end
13
+
14
+ if password != password_confirmation
15
+ message = configuration.resolve(:flash_error_after_password_reset_password_mismatch, {})
16
+ return Result.new(success?: false, errors: Array(message), user: user)
17
+ end
18
+
19
+ configuration.password_setter.call(user, password)
20
+ if user.respond_to?(:password_confirmation=)
21
+ user.password_confirmation = password_confirmation
22
+ end
23
+
24
+ if user.save
25
+ configuration.after_login_success.call(user)
26
+ Result.new(success?: true, user: user, errors: [])
27
+ else
28
+ Result.new(success?: false, user: user, errors: simplified_error_messages(user))
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def simplified_error_messages(model)
35
+ model.errors.details.keys.map { |attr|
36
+ model.errors.full_messages_for(attr).first
37
+ }.compact
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,149 @@
1
+ module Searls
2
+ module Auth
3
+ class UpdatesSettings
4
+ Result = Struct.new(
5
+ :success?,
6
+ :errors,
7
+ :user,
8
+ :password_changed?,
9
+ keyword_init: true
10
+ )
11
+
12
+ def initialize(user:, params:)
13
+ @user = user
14
+ @params = params || {}
15
+ @errors = []
16
+ @password_changed = false
17
+ end
18
+
19
+ def update
20
+ enforce_current_password_requirement
21
+
22
+ handle_password_change if errors.empty?
23
+
24
+ return failure_result unless errors.empty?
25
+
26
+ if changes_applied?
27
+ if user.save
28
+ Result.new(
29
+ success?: true,
30
+ user: user,
31
+ errors: [],
32
+ password_changed?: @password_changed
33
+ )
34
+ else
35
+ Result.new(
36
+ success?: false,
37
+ user: user,
38
+ errors: simplified_error_messages(user)
39
+ )
40
+ end
41
+ else
42
+ Result.new(success?: true, user: user, errors: [], password_changed?: false)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :user, :params, :errors
49
+
50
+ def enforce_current_password_requirement
51
+ return unless password_present?
52
+ return unless password_change_requested?
53
+
54
+ if current_password.blank?
55
+ errors << array_wrap(Searls::Auth.config.resolve(:flash_error_after_settings_current_password_missing, {}))
56
+ errors.flatten!
57
+ return
58
+ end
59
+
60
+ begin
61
+ verified = Searls::Auth.config.password_verifier.call(user, current_password)
62
+ rescue NameError
63
+ errors << array_wrap(Searls::Auth.config.resolve(:flash_error_after_password_misconfigured, {}))
64
+ errors.flatten!
65
+ return
66
+ end
67
+
68
+ unless verified
69
+ errors << array_wrap(Searls::Auth.config.resolve(:flash_error_after_settings_current_password_invalid, {}))
70
+ errors.flatten!
71
+ end
72
+ end
73
+
74
+ def handle_password_change
75
+ return unless password_change_requested?
76
+
77
+ if new_password.blank?
78
+ errors << array_wrap(Searls::Auth.config.resolve(:flash_error_after_password_reset_password_blank, {}))
79
+ errors.flatten!
80
+ return
81
+ end
82
+
83
+ if new_password != new_password_confirmation
84
+ errors << array_wrap(Searls::Auth.config.resolve(:flash_error_after_password_reset_password_mismatch, {}))
85
+ errors.flatten!
86
+ return
87
+ end
88
+
89
+ Searls::Auth.config.password_setter.call(user, new_password)
90
+ if user.respond_to?(:password_confirmation=)
91
+ user.password_confirmation = new_password_confirmation
92
+ end
93
+ @password_changed = true
94
+ end
95
+
96
+ def password_present?
97
+ Searls::Auth.config.password_present?(user)
98
+ end
99
+
100
+ def password_change_requested?
101
+ new_password.present? || new_password_confirmation.present?
102
+ end
103
+
104
+ def current_password
105
+ param(:current_password).to_s
106
+ end
107
+
108
+ def new_password
109
+ param(:password)
110
+ end
111
+
112
+ def new_password_confirmation
113
+ param(:password_confirmation)
114
+ end
115
+
116
+ def param(key)
117
+ params[key] || params[key.to_s]
118
+ end
119
+
120
+ def changes_applied?
121
+ @password_changed
122
+ end
123
+
124
+ def failure_result
125
+ flattened = errors.flatten.compact_blank
126
+ Result.new(success?: false, user: user, errors: flattened.presence || simplified_error_messages(user))
127
+ end
128
+
129
+ def array_wrap(value)
130
+ case value
131
+ when Array
132
+ value
133
+ when nil
134
+ []
135
+ else
136
+ [value]
137
+ end
138
+ end
139
+
140
+ def simplified_error_messages(model)
141
+ return [] unless model.respond_to?(:errors)
142
+
143
+ model.errors.details.keys.map do |attr|
144
+ model.errors.full_messages_for(attr).first
145
+ end.compact
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,5 +1,5 @@
1
1
  module Searls
2
2
  module Auth
3
- VERSION = "0.2.0"
3
+ VERSION = "1.0.1"
4
4
  end
5
5
  end
data/lib/searls/auth.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  require_relative "auth/authenticates_user"
2
+ require_relative "auth/parses_time_safely"
2
3
  require_relative "auth/config"
4
+ require_relative "auth/builds_target_redirect_url" if defined?(Rails)
3
5
  require_relative "auth/creates_user" if defined?(Rails)
4
6
  require_relative "auth/emails_link"
7
+ require_relative "auth/emails_verification"
5
8
  require_relative "auth/engine" if defined?(Rails)
6
9
  require_relative "auth/railtie" if defined?(Rails)
7
10
  require_relative "auth/resets_session"
11
+ require_relative "auth/delivers_password_reset"
12
+ require_relative "auth/resets_password"
13
+ require_relative "auth/updates_settings"
8
14
  require_relative "auth/version"
9
15
 
10
16
  module Searls
@@ -13,6 +19,7 @@ module Searls
13
19
 
14
20
  DEFAULT_CONFIG = {
15
21
  auth_methods: [:email_link, :email_otp],
22
+ email_verification_mode: :none,
16
23
  # Data setup
17
24
  user_finder_by_email: ->(email) { User.find_by(email:) },
18
25
  user_finder_by_id: ->(id) { User.find_by(id:) },
@@ -20,34 +27,52 @@ module Searls
20
27
  user_initializer: ->(params) { User.new(email: params[:email]) },
21
28
  user_name_method: "name",
22
29
  token_generator: ->(user) { user.generate_token_for(:email_auth) },
23
- token_expiry_minutes: 30,
30
+ email_otp_expiry_minutes: 30,
31
+ password_verifier: ->(user, password) { user.authenticate(password) },
32
+ password_setter: ->(user, password) { user.password = password },
33
+ password_reset_token_generator: ->(user) { user.generate_token_for(:password_reset) },
34
+ password_reset_token_finder: ->(token) { User.find_by_token_for(:password_reset, token) },
35
+ before_password_reset: ->(user, params, controller) { true },
36
+ password_reset_enabled: true,
37
+ email_verified_predicate: ->(user) { user.respond_to?(:email_verified_at) && user.email_verified_at.present? },
38
+ email_verified_setter: ->(user, time = Time.current) { user.respond_to?(:email_verified_at) ? user.update!(email_verified_at: time) : true },
39
+ password_present_predicate: ->(user) { user.respond_to?(:password_digest) && user.password_digest.present? },
24
40
  # Controller setup
25
41
  preserve_session_keys_after_logout: [],
26
- max_allowed_short_code_attempts: 10,
42
+ max_allowed_email_otp_attempts: 10,
27
43
  # View setup
28
44
  layout: "application",
29
45
  register_view: "searls/auth/registrations/show",
30
46
  login_view: "searls/auth/logins/show",
31
47
  verify_view: "searls/auth/verifications/show",
48
+ pending_email_verification_view: "searls/auth/registrations/pending_email_verification",
49
+ password_reset_request_view: "searls/auth/requests_password_resets/show",
50
+ password_reset_edit_view: "searls/auth/resets_passwords/show",
32
51
  mail_layout: "searls/auth/layouts/mailer",
33
52
  mail_login_template_path: "searls/auth/login_link_mailer",
34
53
  mail_login_template_name: "login_link",
54
+ mail_password_reset_template_path: "searls/auth/password_reset_mailer",
55
+ mail_password_reset_template_name: "password_reset",
56
+ mail_email_verification_template_path: "searls/auth/email_verification_mailer",
57
+ mail_email_verification_template_name: "verification_email",
35
58
  # Route setup
36
59
  redirect_path_after_register: ->(user, params, request, routes) {
37
60
  # Not every app defines a root_path, so guarding here:
38
61
  routes.respond_to?(:root_path) ? routes.root_path : "/"
39
62
  },
40
- default_redirect_path_after_login: ->(user, params, request, routes) {
63
+ redirect_path_after_login: ->(user, params, request, routes) {
41
64
  # Not every app defines a root_path, so guarding here:
42
65
  routes.respond_to?(:root_path) ? routes.root_path : "/"
43
66
  },
67
+ redirect_path_after_settings_change: ->(user, params, request, routes) {
68
+ routes.respond_to?(:edit_settings_path) ? routes.edit_settings_path : "/settings"
69
+ },
44
70
  # Hook setup
45
71
  validate_registration: ->(user, params, errors) { errors },
46
- after_login_success: nil,
72
+ after_login_success: ->(user) {},
47
73
  # Branding setup
48
74
  app_name: nil,
49
75
  app_url: nil,
50
- support_email_address: nil,
51
76
  email_background_color: "#d8d7ed",
52
77
  email_button_color: "#c664f3",
53
78
  email_banner_image_path: nil,
@@ -56,23 +81,47 @@ module Searls
56
81
  flash_error_after_register_attempt: ->(error_messages, login_path, params) { error_messages },
57
82
  flash_notice_after_login_attempt: ->(user, params) { "Login details sent to #{params[:email]}" },
58
83
  flash_error_after_login_attempt_unknown_email: ->(register_path, params) {
59
- "We don't know that email. <a href=\"#{register_path}\">Sign up</a> instead?".html_safe
84
+ "We don't know that email. <a href=\"#{register_path}\">Sign up</a> instead?"
85
+ },
86
+ flash_error_after_login_attempt_invalid_password: ->(params) { "Invalid password. Try again?" },
87
+ flash_error_after_login_attempt_unverified_email: ->(resend_path, params) {
88
+ "You must verify your email before logging in. <a href=\"#{resend_path}\" data-turbo-method=\"patch\">Resend verification email</a>"
89
+ },
90
+ flash_notice_after_login_with_unverified_email: ->(resend_path, params) {
91
+ "You are now logged in, but your email is still unverified. <a href=\"#{resend_path}\" data-turbo-method=\"patch\">Resend verification email</a>"
92
+ },
93
+ flash_error_after_password_misconfigured: ->(params) {
94
+ "Password authentication misconfigured. Add `bcrypt` to your Gemfile or override password hooks."
60
95
  },
96
+ flash_error_after_password_reset_token_invalid: ->(params) { "That password reset link is no longer valid. Try again?" },
97
+ flash_error_after_password_reset_password_mismatch: ->(params) { "Passwords must match. Try again?" },
98
+ flash_error_after_password_reset_password_blank: ->(params) { "Password can't be blank. Try again?" },
99
+ flash_error_after_password_reset_not_enabled: ->(params) { "Password resets are unavailable." },
61
100
  flash_notice_after_logout: "You've been logged out",
62
- flash_notice_after_verification: "You are now logged in",
101
+ flash_notice_after_login: "You are now logged in",
102
+ flash_notice_after_verification_email_resent: "Verification email sent",
103
+ flash_notice_after_email_verified: "Email verified",
104
+ flash_notice_after_password_reset_email: ->(params) { "If that email exists, password reset instructions are on the way." },
105
+ flash_notice_after_password_reset: ->(user, params) { "Your password has been reset." },
63
106
  flash_error_after_verify_attempt_exceeds_limit: "Too many verification attempts. Please login again to generate a new code",
64
- flash_error_after_verify_attempt_incorrect_short_code: "We weren't able to log you in with that code. Try again?",
65
- flash_error_after_verify_attempt_invalid_link: "We weren't able to log you in with that link. Try again?"
107
+ flash_error_after_verify_attempt_incorrect_email_otp: "We weren't able to log you in with that code. Try again?",
108
+ flash_error_after_verify_attempt_invalid_link: "We weren't able to log you in with that link. Try again?",
109
+ flash_notice_after_settings_update: ->(user, params) { "Settings updated." },
110
+ flash_error_after_settings_current_password_missing: ->(params) { "Enter your current password to make changes." },
111
+ flash_error_after_settings_current_password_invalid: ->(params) { "That current password doesn't match our records." },
112
+ auto_login_after_password_reset: true
66
113
 
67
114
  }.freeze
68
115
 
69
- CONFIG = Config.new(**DEFAULT_CONFIG)
70
- def self.configure(&blk)
71
- yield CONFIG
116
+ C_O_N_F_I_G__D_O_N_T_R_E_F_E_R_E_N_C_E__T_H_I_S__D_I_R_E_C_T_L_Y_L_O_L = Config.new(**DEFAULT_CONFIG)
117
+ def self.configure
118
+ yield C_O_N_F_I_G__D_O_N_T_R_E_F_E_R_E_N_C_E__T_H_I_S__D_I_R_E_C_T_L_Y_L_O_L
119
+ C_O_N_F_I_G__D_O_N_T_R_E_F_E_R_E_N_C_E__T_H_I_S__D_I_R_E_C_T_L_Y_L_O_L.validate!
120
+ C_O_N_F_I_G__D_O_N_T_R_E_F_E_R_E_N_C_E__T_H_I_S__D_I_R_E_C_T_L_Y_L_O_L
72
121
  end
73
122
 
74
123
  def self.config
75
- CONFIG.dup.freeze
124
+ C_O_N_F_I_G__D_O_N_T_R_E_F_E_R_E_N_C_E__T_H_I_S__D_I_R_E_C_T_L_Y_L_O_L.dup.freeze
76
125
  end
77
126
  end
78
127
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searls-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -35,30 +35,52 @@ files:
35
35
  - README.md
36
36
  - Rakefile
37
37
  - app/controllers/searls/auth/base_controller.rb
38
+ - app/controllers/searls/auth/email_verifications_controller.rb
38
39
  - app/controllers/searls/auth/logins_controller.rb
39
40
  - app/controllers/searls/auth/registrations_controller.rb
41
+ - app/controllers/searls/auth/requests_password_resets_controller.rb
42
+ - app/controllers/searls/auth/resets_passwords_controller.rb
43
+ - app/controllers/searls/auth/settings_controller.rb
40
44
  - app/controllers/searls/auth/verifications_controller.rb
41
45
  - app/helpers/searls/auth/application_helper.rb
42
46
  - app/javascript/controllers/searls_auth_login_controller.js
43
47
  - app/javascript/controllers/searls_auth_otp_controller.js
44
48
  - app/mailers/searls/auth/base_mailer.rb
49
+ - app/mailers/searls/auth/email_verification_mailer.rb
45
50
  - app/mailers/searls/auth/login_link_mailer.rb
51
+ - app/mailers/searls/auth/password_reset_mailer.rb
52
+ - app/views/searls/auth/email_verification_mailer/verification_email.html.erb
53
+ - app/views/searls/auth/email_verification_mailer/verification_email.text.erb
46
54
  - app/views/searls/auth/layouts/mailer.html.erb
47
55
  - app/views/searls/auth/login_link_mailer/login_link.html.erb
48
56
  - app/views/searls/auth/login_link_mailer/login_link.text.erb
49
57
  - app/views/searls/auth/logins/show.html.erb
58
+ - app/views/searls/auth/password_reset_mailer/password_reset.html.erb
59
+ - app/views/searls/auth/password_reset_mailer/password_reset.text.erb
60
+ - app/views/searls/auth/registrations/pending_email_verification.html.erb
50
61
  - app/views/searls/auth/registrations/show.html.erb
62
+ - app/views/searls/auth/requests_password_resets/show.html.erb
63
+ - app/views/searls/auth/resets_passwords/show.html.erb
64
+ - app/views/searls/auth/settings/edit.html.erb
65
+ - app/views/searls/auth/shared/_login_fields.html.erb
66
+ - app/views/searls/auth/shared/_register_fields.html.erb
51
67
  - app/views/searls/auth/verifications/show.html.erb
52
68
  - config/importmap.rb
53
69
  - config/routes.rb
54
70
  - lib/searls/auth.rb
55
71
  - lib/searls/auth/authenticates_user.rb
72
+ - lib/searls/auth/builds_target_redirect_url.rb
56
73
  - lib/searls/auth/config.rb
57
74
  - lib/searls/auth/creates_user.rb
75
+ - lib/searls/auth/delivers_password_reset.rb
58
76
  - lib/searls/auth/emails_link.rb
77
+ - lib/searls/auth/emails_verification.rb
59
78
  - lib/searls/auth/engine.rb
79
+ - lib/searls/auth/parses_time_safely.rb
60
80
  - lib/searls/auth/railtie.rb
81
+ - lib/searls/auth/resets_password.rb
61
82
  - lib/searls/auth/resets_session.rb
83
+ - lib/searls/auth/updates_settings.rb
62
84
  - lib/searls/auth/version.rb
63
85
  - script/setup
64
86
  - script/setup_ci