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,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