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.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +18 -0
- data/README.rdoc +484 -0
- data/Rakefile +91 -0
- data/lib/roda/plugins/rodauth.rb +265 -0
- data/lib/roda/plugins/rodauth/base.rb +428 -0
- data/lib/roda/plugins/rodauth/change_login.rb +48 -0
- data/lib/roda/plugins/rodauth/change_password.rb +42 -0
- data/lib/roda/plugins/rodauth/close_account.rb +42 -0
- data/lib/roda/plugins/rodauth/create_account.rb +92 -0
- data/lib/roda/plugins/rodauth/lockout.rb +292 -0
- data/lib/roda/plugins/rodauth/login.rb +77 -0
- data/lib/roda/plugins/rodauth/logout.rb +36 -0
- data/lib/roda/plugins/rodauth/remember.rb +226 -0
- data/lib/roda/plugins/rodauth/reset_password.rb +205 -0
- data/lib/roda/plugins/rodauth/verify_account.rb +228 -0
- data/spec/migrate/001_tables.rb +64 -0
- data/spec/migrate_password/001_tables.rb +38 -0
- data/spec/rodauth_spec.rb +1114 -0
- data/spec/views/layout.str +11 -0
- data/spec/views/login.str +21 -0
- data/templates/change-login.str +22 -0
- data/templates/change-password.str +21 -0
- data/templates/close-account.str +9 -0
- data/templates/confirm-password.str +16 -0
- data/templates/create-account.str +33 -0
- data/templates/login.str +25 -0
- data/templates/logout.str +9 -0
- data/templates/remember.str +28 -0
- data/templates/reset-password-email.str +5 -0
- data/templates/reset-password-request.str +7 -0
- data/templates/reset-password.str +23 -0
- data/templates/unlock-account-email.str +5 -0
- data/templates/unlock-account-request.str +11 -0
- data/templates/unlock-account.str +11 -0
- data/templates/verify-account-email.str +4 -0
- data/templates/verify-account-resend.str +7 -0
- data/templates/verify-account.str +11 -0
- 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
|