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