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,48 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ ChangeLogin = Feature.define(:change_login) do
5
+ route 'change-login'
6
+ notice_flash 'Your login has been changed'
7
+ error_flash 'There was an error changing your login'
8
+ view 'change-login', 'Change Login'
9
+ after
10
+ additional_form_tags
11
+ button 'Change Login'
12
+ redirect
13
+ require_account
14
+
15
+ auth_methods :change_login
16
+
17
+ get_block do |r, auth|
18
+ auth.view('change-login', 'Change Login')
19
+ end
20
+
21
+ post_block do |r, auth|
22
+ if r[auth.login_param] == r[auth.login_confirm_param]
23
+ auth.transaction do
24
+ if auth.change_login(r[auth.login_param].to_s)
25
+ auth.after_change_login
26
+ auth.set_notice_flash auth.change_login_notice_flash
27
+ r.redirect(auth.change_login_redirect)
28
+ else
29
+ @login_error = auth.login_errors_message
30
+ end
31
+ end
32
+ else
33
+ @login_error = auth.logins_do_not_match_message
34
+ end
35
+
36
+ auth.set_error_flash auth.change_login_error_flash
37
+ auth.change_login_view
38
+ end
39
+
40
+ def change_login(login)
41
+ account.set(login_column=>login).save_changes(:raise_on_failure=>false)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+
@@ -0,0 +1,42 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ ChangePassword = Feature.define(:change_password) do
5
+ route 'change-password'
6
+ notice_flash 'Your password has been changed'
7
+ error_flash 'There was an error changing your password'
8
+ view 'change-password', 'Change Password'
9
+ after
10
+ additional_form_tags
11
+ button 'Change Password'
12
+ redirect
13
+ require_account
14
+
15
+ get_block do |r, auth|
16
+ auth.change_password_view
17
+ end
18
+
19
+ post_block do |r, auth|
20
+ if r[auth.password_param] == r[auth.password_confirm_param]
21
+ if auth.password_meets_requirements?(r[auth.password_param].to_s)
22
+ auth.transaction do
23
+ auth.set_password(r[auth.password_param])
24
+ auth.after_change_password
25
+ end
26
+ auth.set_notice_flash auth.change_password_notice_flash
27
+ r.redirect(auth.change_password_redirect)
28
+ else
29
+ @password_error = auth.password_does_not_meet_requirements_message
30
+ end
31
+ else
32
+ @password_error = auth.passwords_do_not_match_message
33
+ end
34
+
35
+ auth.set_error_flash auth.change_password_error_flash
36
+ auth.change_password_view
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
@@ -0,0 +1,42 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ CloseAccount = Feature.define(:close_account) do
5
+ route 'close-account'
6
+ notice_flash 'Your account has been closed'
7
+ view 'close-account', 'Close Account'
8
+ additional_form_tags
9
+ button 'Close Account'
10
+ redirect
11
+ require_account
12
+
13
+ auth_value_methods :account_closed_status_value
14
+ auth_methods :close_account
15
+
16
+ get_block do |r, auth|
17
+ auth.close_account_view
18
+ end
19
+
20
+ post_block do |r, auth|
21
+ auth.transaction do
22
+ auth.close_account
23
+ auth.after_close_account
24
+ end
25
+ auth.clear_session
26
+
27
+ auth.set_notice_flash auth.close_account_notice_flash
28
+ r.redirect(auth.close_account_redirect)
29
+ end
30
+
31
+ def account_closed_status_value
32
+ 3
33
+ end
34
+
35
+ def close_account
36
+ account.update(account_status_id=>account_closed_status_value)
37
+ account.db[password_hash_table].where(account_id=>account_id_value).delete
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ CreateAccount = Feature.define(:create_account) do
5
+ depends :login
6
+ route 'create-account'
7
+ error_flash "There was an error creating your account"
8
+ view 'create-account', 'Create Account'
9
+ after
10
+ button 'Create Account'
11
+ additional_form_tags
12
+ redirect
13
+
14
+ auth_value_methods :create_account_autologin?, :create_account_link, :create_account_notice_flash
15
+ auth_methods :new_account, :save_account
16
+
17
+ get_block do |r, auth|
18
+ auth.create_account_view
19
+ end
20
+
21
+ post_block do |r, auth|
22
+ login = r[auth.login_param].to_s
23
+ password = r[auth.password_param].to_s
24
+ auth._new_account(login)
25
+ if login == r[auth.login_confirm_param]
26
+ if password == r[auth.password_confirm_param]
27
+ if auth.password_meets_requirements?(password)
28
+ auth.transaction do
29
+ if auth.save_account
30
+ auth.set_password(password) unless auth.account_password_hash_column
31
+ auth.after_create_account
32
+ if auth.create_account_autologin?
33
+ auth.update_session
34
+ end
35
+ auth.set_notice_flash auth.create_account_notice_flash
36
+ r.redirect(auth.create_account_redirect)
37
+ else
38
+ @login_error = auth.login_errors_message
39
+ end
40
+ end
41
+ else
42
+ @password_error = auth.password_does_not_meet_requirements_message
43
+ end
44
+ else
45
+ @password_error = auth.passwords_do_not_match_message
46
+ end
47
+ else
48
+ @login_error = auth.logins_do_not_match_message
49
+ end
50
+
51
+ auth.set_error_flash auth.create_account_error_flash
52
+ auth.create_account_view
53
+ end
54
+
55
+ def create_account_notice_flash
56
+ "Your account has been created"
57
+ end
58
+
59
+ def create_account_link
60
+ "<p><a href=\"#{prefix}/#{create_account_route}\">Create a New Account</a></p>"
61
+ end
62
+
63
+ def login_form_footer
64
+ super + create_account_link
65
+ end
66
+
67
+ def create_account_autologin?
68
+ false
69
+ end
70
+
71
+ def new_account(login)
72
+ @account = account_model.new(login_column=>login)
73
+ if account_password_hash_column
74
+ account.set(account_password_hash_column=>password_hash(request[password_param].to_s))
75
+ end
76
+ unless skip_status_checks?
77
+ account.set(account_status_id=>account_initial_status_value)
78
+ end
79
+ @account
80
+ end
81
+
82
+ def _new_account(login)
83
+ @account = new_account(login)
84
+ end
85
+
86
+ def save_account
87
+ account.save(:raise_on_failure=>false)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,292 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ module Rodauth
4
+ Lockout = Feature.define(:lockout) do
5
+ depends :login
6
+ route 'unlock-account'
7
+
8
+ auth_value_methods(
9
+ :account_lockouts_id_column,
10
+ :account_lockouts_deadline_column,
11
+ :account_lockouts_key_column,
12
+ :account_lockouts_table,
13
+ :account_login_failures_id_column,
14
+ :account_login_failures_number_column,
15
+ :account_login_failures_table,
16
+ :max_invalid_logins,
17
+ :unlock_account_additional_form_tags,
18
+ :unlock_account_autologin?,
19
+ :unlock_account_button,
20
+ :unlock_account_email_subject,
21
+ :unlock_account_key_param,
22
+ :unlock_account_notice_flash,
23
+ :unlock_account_redirect,
24
+ :unlock_account_request_additional_form_tags,
25
+ :unlock_account_request_button,
26
+ :unlock_account_request_notice_flash,
27
+ :unlock_account_request_redirect,
28
+ :unlock_account_route
29
+ )
30
+ auth_methods(
31
+ :after_unlock_account,
32
+ :after_unlock_account_request,
33
+ :clear_invalid_login_attempts,
34
+ :create_unlock_account_email,
35
+ :generate_unlock_account_key,
36
+ :get_unlock_account_key,
37
+ :invalid_login_attempted,
38
+ :locked_out?,
39
+ :send_unlock_account_email,
40
+ :unlock_account_request_view,
41
+ :unlock_account_email_body,
42
+ :unlock_account_email_link,
43
+ :unlock_account,
44
+ :unlock_account_key,
45
+ :unlock_account_view
46
+ )
47
+
48
+ get_block do |r, auth|
49
+ if auth._account_from_unlock_key(r[auth.unlock_account_key_param].to_s)
50
+ auth.unlock_account_view
51
+ else
52
+ auth.set_redirect_error_flash auth.no_matching_unlock_account_key_message
53
+ r.redirect auth.require_login_redirect
54
+ end
55
+ end
56
+
57
+ post_block do |r, auth|
58
+ if login = r[auth.login_param]
59
+ if auth._account_from_login(login.to_s)
60
+ auth.transaction do
61
+ auth.send_unlock_account_email
62
+ auth.after_unlock_account_request
63
+ end
64
+ auth.set_notice_flash auth.unlock_account_request_notice_flash
65
+ r.redirect auth.unlock_account_request_redirect
66
+ end
67
+ elsif key = r[auth.unlock_account_key_param]
68
+ if auth._account_from_unlock_key(key.to_s)
69
+ auth.unlock_account
70
+ auth.after_unlock_account
71
+ if auth.unlock_account_autologin?
72
+ auth.update_session
73
+ end
74
+ auth.set_notice_flash auth.unlock_account_notice_flash
75
+ r.redirect(auth.unlock_account_redirect)
76
+ end
77
+ end
78
+ end
79
+
80
+ def before_login_attempt
81
+ super
82
+ if locked_out?
83
+ set_error_flash login_error_flash
84
+ response.write unlock_account_request_view
85
+ request.halt
86
+ end
87
+ end
88
+
89
+ def after_login
90
+ super
91
+ clear_invalid_login_attempts
92
+ end
93
+
94
+ def after_login_failure
95
+ super
96
+ invalid_login_attempted
97
+ end
98
+
99
+ def after_unlock_account
100
+ end
101
+
102
+ def after_unlock_account_request
103
+ end
104
+
105
+ alias unlock_account_route lockout_route
106
+
107
+ def unlock_account_autologin?
108
+ false
109
+ end
110
+
111
+ def unlock_account_notice_flash
112
+ "Your account has been unlocked"
113
+ end
114
+
115
+ def unlock_account_redirect
116
+ default_redirect
117
+ end
118
+
119
+ def unlock_account_button
120
+ 'Unlock Account'
121
+ end
122
+
123
+ def unlock_account_additional_form_tags
124
+ end
125
+
126
+ def unlock_account_request_notice_flash
127
+ "An email has been sent to you with a link to unlock your account"
128
+ end
129
+
130
+ def unlock_account_request_redirect
131
+ default_redirect
132
+ end
133
+
134
+ def unlock_account_request_button
135
+ 'Request Account Unlock'
136
+ end
137
+
138
+ def unlock_account_request_additional_form_tags
139
+ end
140
+
141
+ # This is solely for bruteforce protection, so we allow 100 tries.
142
+ def max_invalid_logins
143
+ 100
144
+ end
145
+
146
+ def account_login_failures_table
147
+ :account_login_failures
148
+ end
149
+
150
+ def account_login_failures_id_column
151
+ :id
152
+ end
153
+
154
+ def account_login_failures_number_column
155
+ :number
156
+ end
157
+
158
+ def account_login_failures_dataset
159
+ db[account_login_failures_table].where(account_login_failures_id_column=>account_id_value)
160
+ end
161
+
162
+ def account_lockouts_table
163
+ :account_lockouts
164
+ end
165
+
166
+ def account_lockouts_id_column
167
+ :id
168
+ end
169
+
170
+ def account_lockouts_key_column
171
+ :key
172
+ end
173
+
174
+ def account_lockouts_deadline_column
175
+ :deadline
176
+ end
177
+
178
+ def account_lockouts_dataset
179
+ db[account_lockouts_table].where(account_lockouts_id_column=>account_id_value)
180
+ end
181
+
182
+ def locked_out?
183
+ if lockout = account_lockouts_dataset.first
184
+ if Time.now < lockout[account_lockouts_deadline_column]
185
+ true
186
+ else
187
+ unlock_account
188
+ false
189
+ end
190
+ else
191
+ false
192
+ end
193
+ end
194
+
195
+ def unlock_account
196
+ transaction do
197
+ account_login_failures_dataset.delete
198
+ account_lockouts_dataset.delete
199
+ end
200
+ end
201
+
202
+ def unlock_account_request_view
203
+ view('unlock-account-request', 'Request Account Unlock')
204
+ end
205
+
206
+ def unlock_account_view
207
+ view('unlock-account', 'Unlock Account')
208
+ end
209
+
210
+ def no_matching_unlock_account_key_message
211
+ 'No matching unlock account key'
212
+ end
213
+
214
+ def clear_invalid_login_attempts
215
+ unlock_account
216
+ end
217
+
218
+ def invalid_login_attempted
219
+ number = account_login_failures_dataset.
220
+ returning(account_login_failures_number_column).
221
+ where(account_login_failures_id_column=>account_id_value).
222
+ with_sql(:update_sql, account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).
223
+ single_value
224
+
225
+ unless number
226
+ account_login_failures_dataset.insert(account_login_failures_id_column=>account_id_value)
227
+ number = 1
228
+ end
229
+
230
+ if number >= max_invalid_logins
231
+ @unlock_account_key_value = generate_unlock_account_key
232
+ account_lockouts_dataset.insert(account_lockouts_id_column=>account_id_value, account_lockouts_key_column=>unlock_account_key_value)
233
+ end
234
+ end
235
+
236
+ def get_unlock_account_key
237
+ account_lockouts_dataset.get(account_lockouts_key_column)
238
+ end
239
+
240
+ def generate_unlock_account_key
241
+ random_key
242
+ end
243
+
244
+ attr_reader :unlock_account_key_value
245
+
246
+ def _account_from_unlock_key(key)
247
+ @account = account_from_unlock_key(key)
248
+ end
249
+
250
+ def account_from_unlock_key(key)
251
+ id, key = key.split('_', 2)
252
+ id_column = account_lockouts_id_column
253
+ ds = db[account_lockouts_table].
254
+ select(account_lockouts_id_column).
255
+ where(account_lockouts_id_column=>id, account_lockouts_key_column=>key)
256
+ account_model.where(account_id=>ds).first
257
+ end
258
+
259
+ def unlock_account_key_param
260
+ 'key'
261
+ end
262
+
263
+ def create_unlock_account_email
264
+ create_email(unlock_account_email_subject, unlock_account_email_body)
265
+ end
266
+
267
+ def send_unlock_account_email
268
+ @unlock_account_key_value = get_unlock_account_key
269
+ create_unlock_account_email.deliver!
270
+ end
271
+
272
+ def unlock_account_email_body
273
+ render('unlock-account-email')
274
+ end
275
+
276
+ def unlock_account_email_link
277
+ "#{request.base_url}#{prefix}/#{unlock_account_route}?#{unlock_account_key_param}=#{account_id_value}_#{unlock_account_key_value}"
278
+ end
279
+
280
+ def unlock_account_email_subject
281
+ 'Unlock Account'
282
+ end
283
+
284
+ def after_close_account
285
+ super
286
+ account_login_failures_dataset.delete
287
+ account_lockouts_dataset.delete
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end