rodauth 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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