rodauth 0.9.0

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +3 -0
  3. data/MIT-LICENSE +18 -0
  4. data/README.rdoc +484 -0
  5. data/Rakefile +91 -0
  6. data/lib/roda/plugins/rodauth.rb +265 -0
  7. data/lib/roda/plugins/rodauth/base.rb +428 -0
  8. data/lib/roda/plugins/rodauth/change_login.rb +48 -0
  9. data/lib/roda/plugins/rodauth/change_password.rb +42 -0
  10. data/lib/roda/plugins/rodauth/close_account.rb +42 -0
  11. data/lib/roda/plugins/rodauth/create_account.rb +92 -0
  12. data/lib/roda/plugins/rodauth/lockout.rb +292 -0
  13. data/lib/roda/plugins/rodauth/login.rb +77 -0
  14. data/lib/roda/plugins/rodauth/logout.rb +36 -0
  15. data/lib/roda/plugins/rodauth/remember.rb +226 -0
  16. data/lib/roda/plugins/rodauth/reset_password.rb +205 -0
  17. data/lib/roda/plugins/rodauth/verify_account.rb +228 -0
  18. data/spec/migrate/001_tables.rb +64 -0
  19. data/spec/migrate_password/001_tables.rb +38 -0
  20. data/spec/rodauth_spec.rb +1114 -0
  21. data/spec/views/layout.str +11 -0
  22. data/spec/views/login.str +21 -0
  23. data/templates/change-login.str +22 -0
  24. data/templates/change-password.str +21 -0
  25. data/templates/close-account.str +9 -0
  26. data/templates/confirm-password.str +16 -0
  27. data/templates/create-account.str +33 -0
  28. data/templates/login.str +25 -0
  29. data/templates/logout.str +9 -0
  30. data/templates/remember.str +28 -0
  31. data/templates/reset-password-email.str +5 -0
  32. data/templates/reset-password-request.str +7 -0
  33. data/templates/reset-password.str +23 -0
  34. data/templates/unlock-account-email.str +5 -0
  35. data/templates/unlock-account-request.str +11 -0
  36. data/templates/unlock-account.str +11 -0
  37. data/templates/verify-account-email.str +4 -0
  38. data/templates/verify-account-resend.str +7 -0
  39. data/templates/verify-account.str +11 -0
  40. metadata +227 -0
@@ -0,0 +1,77 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ Login = Feature.define(:login) do
5
+ route 'login'
6
+ notice_flash "You have been logged in"
7
+ error_flash "There was an error logging in"
8
+ view 'login', 'Login'
9
+ after
10
+ additional_form_tags
11
+ button 'Login'
12
+ redirect
13
+
14
+ auth_value_methods :invalid_password_message, :login_form_footer
15
+ auth_methods(
16
+ :after_login_failure,
17
+ :before_login_attempt,
18
+ :password_match?
19
+ )
20
+
21
+ get_block do |r, auth|
22
+ auth.login_view
23
+ end
24
+
25
+ post_block do |r, auth|
26
+ auth.clear_session
27
+
28
+ if auth._account_from_login(r[auth.login_param].to_s)
29
+ auth.before_login_attempt
30
+
31
+ if auth.open_account?
32
+ if auth.password_match?(r[auth.password_param].to_s)
33
+ auth.update_session
34
+ auth.after_login
35
+ auth.set_notice_flash auth.login_notice_flash
36
+ r.redirect auth.login_redirect
37
+ else
38
+ auth.after_login_failure
39
+ @password_error = auth.invalid_password_message
40
+ end
41
+ else
42
+ @login_error = auth.unverified_account_message
43
+ end
44
+ else
45
+ @login_error = auth.no_matching_login_message
46
+ end
47
+
48
+ auth.set_error_flash auth.login_error_flash
49
+ auth.login_view
50
+ end
51
+
52
+ def before_login_attempt
53
+ end
54
+
55
+ def after_login_failure
56
+ end
57
+
58
+ def login_form_footer
59
+ ""
60
+ end
61
+
62
+ def invalid_password_message
63
+ "invalid password"
64
+ end
65
+
66
+ def password_match?(password)
67
+ if account_password_hash_column
68
+ require 'bcrypt'
69
+ BCrypt::Password.new(account.send(account_password_hash_column)) == password
70
+ else
71
+ db.get{|db| db.account_valid_password(account.send(account_id), password)}
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ Logout = Feature.define(:logout) do
5
+ route 'logout'
6
+ notice_flash "You have been logged out"
7
+ view 'logout', 'Logout'
8
+ additional_form_tags
9
+ after
10
+ button 'Logout'
11
+ redirect{require_login_redirect}
12
+
13
+ auth_methods :logout
14
+
15
+ get_block do |r, auth|
16
+ auth.logout_view
17
+ end
18
+
19
+ post_block do |r, auth|
20
+ auth.logout
21
+ auth.after_logout
22
+ auth.set_notice_flash auth.logout_notice_flash
23
+ r.redirect auth.logout_redirect
24
+ end
25
+
26
+ def logout
27
+ clear_session
28
+ end
29
+
30
+ def check_before_logout
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,226 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ Remember = Feature.define(:remember) do
5
+ depends :logout
6
+ route 'remember'
7
+ notice_flash "Your remember setting has been updated"
8
+ view 'remember', 'Change Remember Setting'
9
+ additional_form_tags
10
+ button 'Change Remember Setting'
11
+ after
12
+ redirect
13
+ require_account
14
+
15
+ auth_value_methods(
16
+ :extend_remember_deadline?,
17
+ :remember_confirm_view,
18
+ :remember_confirm_additional_form_tags,
19
+ :remember_cookie_key,
20
+ :remember_cookie_options,
21
+ :remember_deadline_column,
22
+ :remember_id_column,
23
+ :remember_key_column,
24
+ :remember_period,
25
+ :remember_table,
26
+ :remembered_session_key
27
+ )
28
+ auth_methods(
29
+ :add_remember_key,
30
+ :after_load_memory,
31
+ :after_remember_confirm,
32
+ :clear_remembered_session_key,
33
+ :disable_remember_login,
34
+ :forget_login,
35
+ :generate_remember_key_value,
36
+ :get_remember_key,
37
+ :load_memory,
38
+ :logged_in_via_remember_key?,
39
+ :remember_key_value,
40
+ :remember_login,
41
+ :remove_remember_key
42
+ )
43
+
44
+ get_block do |r, auth|
45
+ if r['confirm']
46
+ auth.remember_confirm_view
47
+ else
48
+ auth.remember_view
49
+ end
50
+ end
51
+
52
+ post_block do |r, auth|
53
+ if r['confirm']
54
+ if auth._account_from_session && auth.password_match?(r[auth.password_param].to_s)
55
+ auth.transaction do
56
+ auth.clear_remembered_session_key
57
+ auth.after_remember_confirm
58
+ end
59
+ r.redirect auth.remember_confirm_redirect
60
+ else
61
+ @password_error = auth.invalid_password_message
62
+ auth.remember_confirm_view
63
+ end
64
+ else
65
+ auth.transaction do
66
+ case r['remember']
67
+ when 'remember'
68
+ auth.remember_login
69
+ when 'forget'
70
+ auth.forget_login
71
+ when 'disable'
72
+ auth.disable_remember_login
73
+ end
74
+ auth.after_remember
75
+ end
76
+ auth.set_notice_flash auth.remember_notice_flash
77
+ r.redirect auth.remember_redirect
78
+ end
79
+ end
80
+
81
+ def after_logout
82
+ super
83
+ forget_login
84
+ end
85
+
86
+ def after_remember_confirm
87
+ end
88
+
89
+ def remember_confirm_view
90
+ view('confirm-password', 'Confirm Password')
91
+ end
92
+
93
+ def remember_confirm_button
94
+ 'Confirm Password'
95
+ end
96
+
97
+ def remember_confirm_redirect
98
+ default_redirect
99
+ end
100
+
101
+ def remember_confirm_additional_form_tags
102
+ end
103
+
104
+ attr_reader :remember_key_value
105
+
106
+ def generate_remember_key_value
107
+ @remember_key_value = random_key
108
+ end
109
+
110
+ def after_load_memory
111
+ end
112
+
113
+ def load_memory
114
+ if !session[session_key] && (cookie = request.cookies[remember_cookie_key])
115
+ id, key = cookie.split('_', 2)
116
+ if id && key
117
+ id = id.to_i
118
+ if session[session_key] = active_remember_key_dataset(id).
119
+ where(remember_key_column=>key.to_s).
120
+ get(remember_id_column)
121
+ account_from_session
122
+
123
+ session[remembered_session_key] = true
124
+ if extend_remember_deadline?
125
+ active_remember_key_dataset(id).update(:deadline=>Sequel.expr(:deadline) + Sequel.cast(remember_period, :interval))
126
+ end
127
+ after_load_memory
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ def remember_login
134
+ get_remember_key
135
+ opts = Hash[remember_cookie_options]
136
+ opts[:value] = "#{account_id_value}_#{remember_key_value}"
137
+ ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
138
+ end
139
+
140
+ def remember_cookie_options
141
+ {}
142
+ end
143
+
144
+ def extend_remember_deadline?
145
+ false
146
+ end
147
+
148
+ def remember_period
149
+ '2 weeks'
150
+ end
151
+
152
+ def forget_login
153
+ ::Rack::Utils.delete_cookie_header!(response.headers, remember_cookie_key, remember_cookie_options)
154
+ end
155
+
156
+ def remember_key_dataset(id_value=account_id_value)
157
+ db[remember_table].
158
+ where(remember_id_column=>id_value)
159
+ end
160
+ def active_remember_key_dataset(id_value=account_id_value)
161
+ remember_key_dataset(id_value).where(Sequel.expr(remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
162
+ end
163
+
164
+ def get_remember_key
165
+ unless @remember_key_value = active_remember_key_dataset.get(remember_key_column)
166
+ generate_remember_key_value
167
+ transaction do
168
+ remove_remember_key
169
+ add_remember_key
170
+ end
171
+ end
172
+ nil
173
+ end
174
+
175
+ def disable_remember_login
176
+ remove_remember_key
177
+ end
178
+
179
+ def add_remember_key
180
+ remember_key_dataset.insert(remember_id_column=>account_id_value, remember_key_column=>remember_key_value)
181
+ end
182
+
183
+ def remove_remember_key
184
+ remember_key_dataset.delete
185
+ end
186
+
187
+ def remember_id_column
188
+ :id
189
+ end
190
+
191
+ def remember_key_column
192
+ :key
193
+ end
194
+
195
+ def remember_deadline_column
196
+ :deadline
197
+ end
198
+
199
+ def remember_table
200
+ :account_remember_keys
201
+ end
202
+
203
+ def remember_cookie_key
204
+ '_remember'
205
+ end
206
+
207
+ def clear_remembered_session_key
208
+ session.delete(remembered_session_key)
209
+ end
210
+
211
+ def logged_in_via_remember_key?
212
+ !!session[remembered_session_key]
213
+ end
214
+
215
+ def remembered_session_key
216
+ :remembered
217
+ end
218
+
219
+ def after_close_account
220
+ super
221
+ remove_remember_key
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,205 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ ResetPassword = Feature.define(:reset_password) do
5
+ depends :login
6
+ route 'reset-password'
7
+ notice_flash "Your password has been reset"
8
+ error_flash "There was an error resetting your password"
9
+ view 'reset-password', 'Reset Password'
10
+ additional_form_tags
11
+ after
12
+ button 'Reset Password'
13
+ redirect
14
+
15
+ auth_value_methods(
16
+ :no_matching_reset_password_key_message,
17
+ :reset_password_autologin?,
18
+ :reset_password_email_sent_notice_message,
19
+ :reset_password_email_sent_redirect,
20
+ :reset_password_email_subject,
21
+ :reset_password_id_column,
22
+ :reset_password_key_column,
23
+ :reset_password_key_param,
24
+ :reset_password_request_additional_form_tags,
25
+ :reset_password_request_button,
26
+ :reset_password_table
27
+ )
28
+ auth_methods(
29
+ :account_from_reset_password_key,
30
+ :after_reset_password_request,
31
+ :create_reset_password_key,
32
+ :create_reset_password_email,
33
+ :remove_reset_password_key,
34
+ :reset_password_email_body,
35
+ :reset_password_email_link,
36
+ :reset_password_key_insert_hash,
37
+ :reset_password_key_value,
38
+ :send_reset_password_email
39
+ )
40
+
41
+ get_block do |r, auth|
42
+ if key = r[auth.reset_password_key_param]
43
+ if auth._account_from_reset_password_key(key)
44
+ auth.reset_password_view
45
+ else
46
+ auth.set_redirect_error_flash auth.no_matching_reset_password_key_message
47
+ r.redirect auth.require_login_redirect
48
+ end
49
+ end
50
+ end
51
+
52
+ post_block do |r, auth|
53
+ if login = r[auth.login_param]
54
+ if auth._account_from_login(login.to_s) && auth.open_account?
55
+ auth.generate_reset_password_key_value
56
+ auth.transaction do
57
+ auth.create_reset_password_key
58
+ auth.send_reset_password_email
59
+ auth.after_reset_password_request
60
+ end
61
+ auth.set_notice_flash auth.reset_password_email_sent_notice_message
62
+ r.redirect auth.reset_password_email_sent_redirect
63
+ end
64
+ elsif key = r[auth.reset_password_key_param]
65
+ if auth._account_from_reset_password_key(key)
66
+ if r[auth.password_param] == r[auth.password_confirm_param]
67
+ if auth.password_meets_requirements?(r[auth.password_param].to_s)
68
+ auth.transaction do
69
+ auth.set_password(r[auth.password_param])
70
+ auth.remove_reset_password_key
71
+ auth.after_reset_password
72
+ end
73
+ if auth.reset_password_autologin?
74
+ auth.update_session
75
+ end
76
+ auth.set_notice_flash auth.reset_password_notice_flash
77
+ r.redirect(auth.reset_password_redirect)
78
+ else
79
+ @password_error = auth.password_does_not_meet_requirements_message
80
+ end
81
+ else
82
+ @password_error = auth.passwords_do_not_match_message
83
+ end
84
+ auth.set_error_flash auth.reset_password_error_flash
85
+ auth.reset_password_view
86
+ end
87
+ end
88
+ end
89
+
90
+ def after_login_failure
91
+ super
92
+ scope.instance_variable_set(:@login_form_header, render("reset-password-request"))
93
+ end
94
+
95
+ def generate_reset_password_key_value
96
+ @reset_password_key_value = random_key
97
+ end
98
+
99
+ def create_reset_password_key
100
+ ds = db[reset_password_table].where(reset_password_id_column=>account_id_value)
101
+ transaction do
102
+ ds.where{deadline < Sequel::CURRENT_TIMESTAMP}.delete
103
+ if ds.empty?
104
+ ds.insert(reset_password_key_insert_hash)
105
+ end
106
+ end
107
+ end
108
+
109
+ def reset_password_key_insert_hash
110
+ {reset_password_id_column=>account_id_value, reset_password_key_column=>reset_password_key_value}
111
+ end
112
+
113
+ def remove_reset_password_key
114
+ db[reset_password_table].where(reset_password_id_column=>account_id_value).delete
115
+ end
116
+
117
+ def reset_password_email_sent_notice_message
118
+ "An email has been sent to you with a link to reset the password for your account"
119
+ end
120
+
121
+ def no_matching_reset_password_key_message
122
+ "invalid password reset key"
123
+ end
124
+
125
+ def _account_from_reset_password_key(key)
126
+ @account = account_from_reset_password_key(key)
127
+ end
128
+
129
+ def account_from_reset_password_key(key)
130
+ id, key = key.split('_', 2)
131
+ id_column = reset_password_id_column
132
+ rpds = db[reset_password_table].
133
+ select(id_column).
134
+ where(id_column=>id, reset_password_key_column=>key)
135
+ ds = account_model.where(account_id=>rpds)
136
+ ds = ds.where(account_status_id=>account_open_status_value) unless skip_status_checks?
137
+ ds.first
138
+ end
139
+
140
+ def after_reset_password_request
141
+ nil
142
+ end
143
+
144
+ def reset_password_request_button
145
+ 'Request Password Reset'
146
+ end
147
+
148
+ def reset_password_request_additional_form_tags
149
+ nil
150
+ end
151
+
152
+ def reset_password_email_sent_redirect
153
+ default_redirect
154
+ end
155
+
156
+ def reset_password_table
157
+ :account_password_reset_keys
158
+ end
159
+
160
+ def reset_password_id_column
161
+ :id
162
+ end
163
+
164
+ def reset_password_key_column
165
+ :key
166
+ end
167
+
168
+ attr_reader :reset_password_key_value
169
+
170
+ def create_reset_password_email
171
+ create_email(reset_password_email_subject, reset_password_email_body)
172
+ end
173
+
174
+ def send_reset_password_email
175
+ create_reset_password_email.deliver!
176
+ end
177
+
178
+ def reset_password_email_body
179
+ render('reset-password-email')
180
+ end
181
+
182
+ def reset_password_email_link
183
+ "#{request.base_url}#{prefix}/#{reset_password_route}?#{reset_password_key_param}=#{account_id_value}_#{reset_password_key_value}"
184
+ end
185
+
186
+ def reset_password_email_subject
187
+ 'Reset Password'
188
+ end
189
+
190
+ def reset_password_key_param
191
+ 'key'
192
+ end
193
+
194
+ def reset_password_autologin?
195
+ false
196
+ end
197
+
198
+ def after_close_account
199
+ super
200
+ db[reset_password_table].where(reset_password_id_column=>account_id_value).delete
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end