rodauth 0.10.0 → 1.0.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 +4 -4
- data/CHANGELOG +146 -0
- data/README.rdoc +644 -220
- data/Rakefile +99 -11
- data/doc/account_expiration.rdoc +55 -0
- data/doc/base.rdoc +104 -0
- data/doc/change_login.rdoc +29 -0
- data/doc/change_password.rdoc +26 -0
- data/doc/close_account.rdoc +31 -0
- data/doc/confirm_password.rdoc +22 -0
- data/doc/create_account.rdoc +34 -0
- data/doc/disallow_password_reuse.rdoc +37 -0
- data/doc/email_base.rdoc +19 -0
- data/doc/jwt.rdoc +35 -0
- data/doc/lockout.rdoc +83 -0
- data/doc/login.rdoc +27 -0
- data/doc/login_password_requirements_base.rdoc +50 -0
- data/doc/logout.rdoc +21 -0
- data/doc/otp.rdoc +100 -0
- data/doc/password_complexity.rdoc +50 -0
- data/doc/password_expiration.rdoc +52 -0
- data/doc/password_grace_period.rdoc +10 -0
- data/doc/recovery_codes.rdoc +60 -0
- data/doc/release_notes/1.0.0.txt +443 -0
- data/doc/remember.rdoc +82 -0
- data/doc/reset_password.rdoc +70 -0
- data/doc/session_expiration.rdoc +27 -0
- data/doc/single_session.rdoc +43 -0
- data/doc/sms_codes.rdoc +119 -0
- data/doc/two_factor_base.rdoc +27 -0
- data/doc/verify_account.rdoc +70 -0
- data/doc/verify_account_grace_period.rdoc +15 -0
- data/doc/verify_change_login.rdoc +9 -0
- data/lib/roda/plugins/rodauth.rb +3 -262
- data/lib/rodauth.rb +260 -0
- data/lib/rodauth/features/account_expiration.rb +108 -0
- data/lib/rodauth/features/base.rb +479 -0
- data/lib/rodauth/features/change_login.rb +77 -0
- data/lib/rodauth/features/change_password.rb +66 -0
- data/lib/rodauth/features/close_account.rb +82 -0
- data/lib/rodauth/features/confirm_password.rb +51 -0
- data/lib/rodauth/features/create_account.rb +128 -0
- data/lib/rodauth/features/disallow_password_reuse.rb +82 -0
- data/lib/rodauth/features/email_base.rb +63 -0
- data/lib/rodauth/features/jwt.rb +151 -0
- data/lib/rodauth/features/lockout.rb +262 -0
- data/lib/rodauth/features/login.rb +61 -0
- data/lib/rodauth/features/login_password_requirements_base.rb +123 -0
- data/lib/rodauth/features/logout.rb +37 -0
- data/lib/rodauth/features/otp.rb +338 -0
- data/lib/rodauth/features/password_complexity.rb +89 -0
- data/lib/rodauth/features/password_expiration.rb +111 -0
- data/lib/rodauth/features/password_grace_period.rb +46 -0
- data/lib/rodauth/features/recovery_codes.rb +240 -0
- data/lib/rodauth/features/remember.rb +200 -0
- data/lib/rodauth/features/reset_password.rb +207 -0
- data/lib/rodauth/features/session_expiration.rb +55 -0
- data/lib/rodauth/features/single_session.rb +87 -0
- data/lib/rodauth/features/sms_codes.rb +498 -0
- data/lib/rodauth/features/two_factor_base.rb +135 -0
- data/lib/rodauth/features/verify_account.rb +232 -0
- data/lib/rodauth/features/verify_account_grace_period.rb +76 -0
- data/lib/rodauth/features/verify_change_login.rb +20 -0
- data/lib/rodauth/migrations.rb +130 -0
- data/lib/rodauth/version.rb +9 -0
- data/spec/account_expiration_spec.rb +90 -0
- data/spec/all.rb +1 -0
- data/spec/change_login_spec.rb +149 -0
- data/spec/change_password_spec.rb +177 -0
- data/spec/close_account_spec.rb +162 -0
- data/spec/confirm_password_spec.rb +70 -0
- data/spec/create_account_spec.rb +127 -0
- data/spec/disallow_password_reuse_spec.rb +84 -0
- data/spec/lockout_spec.rb +228 -0
- data/spec/login_spec.rb +188 -0
- data/spec/migrate/001_tables.rb +103 -16
- data/spec/migrate/002_account_password_hash_column.rb +11 -0
- data/spec/migrate_password/001_tables.rb +60 -42
- data/spec/migrate_travis/001_tables.rb +116 -0
- data/spec/password_complexity_spec.rb +108 -0
- data/spec/password_expiration_spec.rb +243 -0
- data/spec/password_grace_period_spec.rb +93 -0
- data/spec/remember_spec.rb +424 -0
- data/spec/reset_password_spec.rb +185 -0
- data/spec/rodauth_spec.rb +57 -980
- data/spec/session_expiration_spec.rb +58 -0
- data/spec/single_session_spec.rb +107 -0
- data/spec/spec_helper.rb +202 -0
- data/spec/two_factor_spec.rb +1310 -0
- data/spec/verify_account_grace_period_spec.rb +135 -0
- data/spec/verify_account_spec.rb +142 -0
- data/spec/verify_change_login_spec.rb +46 -0
- data/spec/views/login.str +2 -2
- data/templates/add-recovery-codes.str +2 -0
- data/templates/button.str +5 -0
- data/templates/change-login.str +5 -18
- data/templates/change-password.str +6 -14
- data/templates/close-account.str +3 -6
- data/templates/confirm-password.str +4 -14
- data/templates/create-account.str +6 -30
- data/templates/login-confirm-field.str +6 -0
- data/templates/login-field.str +6 -0
- data/templates/login.str +5 -19
- data/templates/logout.str +2 -6
- data/templates/otp-auth-code-field.str +6 -0
- data/templates/otp-auth.str +8 -0
- data/templates/otp-disable.str +6 -0
- data/templates/otp-setup.str +21 -0
- data/templates/password-confirm-field.str +6 -0
- data/templates/password-field.str +6 -0
- data/templates/recovery-auth.str +12 -0
- data/templates/recovery-codes.str +6 -0
- data/templates/remember.str +8 -12
- data/templates/reset-password-request.str +2 -2
- data/templates/reset-password.str +4 -18
- data/templates/sms-auth.str +6 -0
- data/templates/sms-code-field.str +6 -0
- data/templates/sms-confirm.str +7 -0
- data/templates/sms-disable.str +7 -0
- data/templates/sms-request.str +5 -0
- data/templates/sms-setup.str +12 -0
- data/templates/unlock-account-request.str +3 -7
- data/templates/unlock-account.str +4 -7
- data/templates/verify-account-resend.str +2 -2
- data/templates/verify-account.str +2 -6
- metadata +191 -29
- data/lib/roda/plugins/rodauth/base.rb +0 -428
- data/lib/roda/plugins/rodauth/change_login.rb +0 -48
- data/lib/roda/plugins/rodauth/change_password.rb +0 -42
- data/lib/roda/plugins/rodauth/close_account.rb +0 -42
- data/lib/roda/plugins/rodauth/create_account.rb +0 -92
- data/lib/roda/plugins/rodauth/lockout.rb +0 -292
- data/lib/roda/plugins/rodauth/login.rb +0 -81
- data/lib/roda/plugins/rodauth/logout.rb +0 -36
- data/lib/roda/plugins/rodauth/remember.rb +0 -226
- data/lib/roda/plugins/rodauth/reset_password.rb +0 -205
- data/lib/roda/plugins/rodauth/verify_account.rb +0 -228
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
AccountExpiration = Feature.define(:account_expiration) do
|
|
5
|
+
error_flash "You cannot log into this account as it has expired"
|
|
6
|
+
redirect
|
|
7
|
+
after
|
|
8
|
+
|
|
9
|
+
auth_value_method :account_activity_expired_column, :expired_at
|
|
10
|
+
auth_value_method :account_activity_id_column, :id
|
|
11
|
+
auth_value_method :account_activity_last_activity_column, :last_activity_at
|
|
12
|
+
auth_value_method :account_activity_last_login_column, :last_login_at
|
|
13
|
+
auth_value_method :account_activity_table, :account_activity_times
|
|
14
|
+
auth_value_method :expire_account_after, 180*86400
|
|
15
|
+
auth_value_method :expire_account_on_last_activity?, false
|
|
16
|
+
|
|
17
|
+
auth_methods(
|
|
18
|
+
:account_expired?,
|
|
19
|
+
:account_expired_at,
|
|
20
|
+
:last_account_activity_at,
|
|
21
|
+
:last_account_login_at,
|
|
22
|
+
:set_expired,
|
|
23
|
+
:update_last_activity,
|
|
24
|
+
:update_last_login
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def last_account_activity_at
|
|
28
|
+
get_activity_timestamp(session_value, account_activity_last_activity_column)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def last_account_login_at
|
|
32
|
+
get_activity_timestamp(session_value, account_activity_last_login_column)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def account_expired_at
|
|
36
|
+
get_activity_timestamp(account_id, account_activity_expired_column)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def update_last_login
|
|
40
|
+
update_activity(account_id, account_activity_last_login_column, account_activity_last_activity_column)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def update_last_activity
|
|
44
|
+
if session_value
|
|
45
|
+
update_activity(session_value, account_activity_last_activity_column)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_expired
|
|
50
|
+
update_activity(account_id, account_activity_expired_column)
|
|
51
|
+
after_account_expiration
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def account_expired?
|
|
55
|
+
columns = [account_activity_last_activity_column, account_activity_last_login_column, account_activity_expired_column]
|
|
56
|
+
last_activity, last_login, expired = account_activity_ds(account_id).get(columns)
|
|
57
|
+
return true if expired
|
|
58
|
+
timestamp = convert_timestamp(expire_account_on_last_activity? ? last_activity : last_login)
|
|
59
|
+
return false unless timestamp
|
|
60
|
+
timestamp < Time.now - expire_account_after
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_account_expiration
|
|
64
|
+
if account_expired?
|
|
65
|
+
set_expired unless account_expired_at
|
|
66
|
+
set_redirect_error_flash account_expiration_error_flash
|
|
67
|
+
redirect account_expiration_redirect
|
|
68
|
+
end
|
|
69
|
+
update_last_login
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def after_close_account
|
|
75
|
+
super if defined?(super)
|
|
76
|
+
account_activity_ds(account_id).delete
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update_session
|
|
80
|
+
check_account_expiration
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def account_activity_ds(account_id)
|
|
85
|
+
db[account_activity_table].
|
|
86
|
+
where(account_activity_id_column=>account_id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def get_activity_timestamp(account_id, column)
|
|
90
|
+
convert_timestamp(account_activity_ds(account_id).get(column))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def update_activity(account_id, *columns)
|
|
94
|
+
ds = account_activity_ds(account_id)
|
|
95
|
+
hash = {}
|
|
96
|
+
columns.each do |c|
|
|
97
|
+
hash[c] = Sequel::CURRENT_TIMESTAMP
|
|
98
|
+
end
|
|
99
|
+
if ds.update(hash) == 0
|
|
100
|
+
hash[account_activity_id_column] = account_id
|
|
101
|
+
hash[account_activity_last_activity_column] ||= Sequel::CURRENT_TIMESTAMP
|
|
102
|
+
hash[account_activity_last_login_column] ||= Sequel::CURRENT_TIMESTAMP
|
|
103
|
+
# It is safe to ignore uniqueness violations here, as a concurrent insert would also use current timestamps.
|
|
104
|
+
ignore_uniqueness_violation{ds.insert(hash)}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Base = Feature.define(:base) do
|
|
5
|
+
before 'rodauth'
|
|
6
|
+
|
|
7
|
+
error_flash "Please login to continue", 'require_login'
|
|
8
|
+
|
|
9
|
+
auth_value_method :account_id_column, :id
|
|
10
|
+
auth_value_method :account_open_status_value, 2
|
|
11
|
+
auth_value_method :account_password_hash_column, nil
|
|
12
|
+
auth_value_method :account_select, nil
|
|
13
|
+
auth_value_method :account_status_column, :status_id
|
|
14
|
+
auth_value_method :account_unverified_status_value, 1
|
|
15
|
+
auth_value_method :accounts_table, :accounts
|
|
16
|
+
auth_value_method :default_redirect, '/'
|
|
17
|
+
auth_value_method :invalid_password_message, "invalid password"
|
|
18
|
+
auth_value_method :login_column, :email
|
|
19
|
+
auth_value_method :password_hash_id_column, :id
|
|
20
|
+
auth_value_method :password_hash_column, :password_hash
|
|
21
|
+
auth_value_method :password_hash_table, :account_password_hashes
|
|
22
|
+
auth_value_method :no_matching_login_message, "no matching login"
|
|
23
|
+
auth_value_method :login_param, 'login'
|
|
24
|
+
auth_value_method :login_label, 'Login'
|
|
25
|
+
auth_value_method :password_label, 'Password'
|
|
26
|
+
auth_value_method :password_param, 'password'
|
|
27
|
+
auth_value_method :modifications_require_password?, true
|
|
28
|
+
auth_value_method :session_key, :account_id
|
|
29
|
+
auth_value_method :prefix, ''
|
|
30
|
+
auth_value_method :require_bcrypt?, true
|
|
31
|
+
auth_value_method :skip_status_checks?, true
|
|
32
|
+
auth_value_method :title_instance_variable, nil
|
|
33
|
+
auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
|
|
34
|
+
|
|
35
|
+
redirect(:require_login){"#{prefix}/login"}
|
|
36
|
+
|
|
37
|
+
auth_value_methods(
|
|
38
|
+
:db,
|
|
39
|
+
:require_login_redirect,
|
|
40
|
+
:set_deadline_values?,
|
|
41
|
+
:use_date_arithmetic?,
|
|
42
|
+
:use_database_authentication_functions?
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
auth_methods(
|
|
46
|
+
:account_id,
|
|
47
|
+
:account_session_value,
|
|
48
|
+
:already_logged_in,
|
|
49
|
+
:authenticated?,
|
|
50
|
+
:clear_session,
|
|
51
|
+
:csrf_tag,
|
|
52
|
+
:function_name,
|
|
53
|
+
:logged_in?,
|
|
54
|
+
:login_required,
|
|
55
|
+
:open_account?,
|
|
56
|
+
:password_match?,
|
|
57
|
+
:random_key,
|
|
58
|
+
:redirect,
|
|
59
|
+
:session_value,
|
|
60
|
+
:set_error_flash,
|
|
61
|
+
:set_notice_flash,
|
|
62
|
+
:set_notice_now_flash,
|
|
63
|
+
:set_redirect_error_flash,
|
|
64
|
+
:set_title,
|
|
65
|
+
:unverified_account_message,
|
|
66
|
+
:update_session
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
auth_private_methods(
|
|
70
|
+
:account_from_login,
|
|
71
|
+
:account_from_session
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
configuration_module_eval do
|
|
75
|
+
def auth_class_eval(&block)
|
|
76
|
+
auth.class_eval(&block)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def account_model(model)
|
|
80
|
+
warn "account_model is deprecated, use db and accounts_table settings"
|
|
81
|
+
db model.db
|
|
82
|
+
accounts_table model.table_name
|
|
83
|
+
account_select model.dataset.opts[:select]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
attr_reader :scope
|
|
88
|
+
attr_reader :account
|
|
89
|
+
|
|
90
|
+
def initialize(scope)
|
|
91
|
+
@scope = scope
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def features
|
|
95
|
+
self.class.features
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def request
|
|
99
|
+
scope.request
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def response
|
|
103
|
+
scope.response
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def session
|
|
107
|
+
scope.session
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def flash
|
|
111
|
+
scope.flash
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def route!
|
|
115
|
+
if meth = self.class.route_hash[request.remaining_path]
|
|
116
|
+
send(meth)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def set_field_error(field, error)
|
|
123
|
+
(@field_errors ||= {})[field] = error
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def field_error(field)
|
|
127
|
+
return nil unless @field_errors
|
|
128
|
+
@field_errors[field]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def account_id
|
|
132
|
+
account[account_id_column]
|
|
133
|
+
end
|
|
134
|
+
alias account_session_value account_id
|
|
135
|
+
|
|
136
|
+
def session_value
|
|
137
|
+
session[session_key]
|
|
138
|
+
end
|
|
139
|
+
alias logged_in? session_value
|
|
140
|
+
|
|
141
|
+
def account_from_login(login)
|
|
142
|
+
@account = _account_from_login(login)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def open_account?
|
|
146
|
+
skip_status_checks? || account[account_status_column] == account_open_status_value
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def db
|
|
150
|
+
Sequel::DATABASES.first
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# If the account_password_hash_column is set, the password hash is verified in
|
|
154
|
+
# ruby, it will not use a database function to do so, it will check the password
|
|
155
|
+
# hash using bcrypt.
|
|
156
|
+
def account_password_hash_column
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def check_already_logged_in
|
|
161
|
+
already_logged_in if logged_in?
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def already_logged_in
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def clear_session
|
|
169
|
+
session.clear
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def login_required
|
|
173
|
+
set_redirect_error_flash require_login_error_flash
|
|
174
|
+
redirect require_login_redirect
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def set_title(title)
|
|
178
|
+
if title_instance_variable
|
|
179
|
+
scope.instance_variable_set(title_instance_variable, title)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def set_error_flash(message)
|
|
184
|
+
flash.now[:error] = message
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def set_redirect_error_flash(message)
|
|
188
|
+
flash[:error] = message
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def set_notice_flash(message)
|
|
192
|
+
flash[:notice] = message
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def set_notice_now_flash(message)
|
|
196
|
+
flash.now[:notice] = message
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def require_login
|
|
200
|
+
login_required unless logged_in?
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def authenticated?
|
|
204
|
+
logged_in?
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def require_authentication
|
|
208
|
+
require_login
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def account_initial_status_value
|
|
212
|
+
account_open_status_value
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def account_from_session
|
|
216
|
+
@account = _account_from_session
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def csrf_tag
|
|
220
|
+
scope.csrf_tag if scope.respond_to?(:csrf_tag)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def button(value, opts={})
|
|
224
|
+
opts = {:locals=>{:value=>value, :opts=>opts}}
|
|
225
|
+
opts[:path] = template_path('button')
|
|
226
|
+
scope.render(opts)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def view(page, title)
|
|
230
|
+
set_title(title)
|
|
231
|
+
_view(:view, page)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def render(page)
|
|
235
|
+
_view(:render, page)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def post_configure
|
|
239
|
+
require 'bcrypt' if require_bcrypt?
|
|
240
|
+
db.extension :date_arithmetic if use_date_arithmetic?
|
|
241
|
+
route_hash= {}
|
|
242
|
+
self.class.routes.each do |meth|
|
|
243
|
+
route_hash["/#{send("#{meth.to_s.sub(/\Ahandle_/, '')}_route")}"] = meth
|
|
244
|
+
end
|
|
245
|
+
self.class.route_hash = route_hash.freeze
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def password_match?(password)
|
|
249
|
+
if account_password_hash_column
|
|
250
|
+
BCrypt::Password.new(account[account_password_hash_column]) == password
|
|
251
|
+
elsif use_database_authentication_functions?
|
|
252
|
+
id = account_id
|
|
253
|
+
if salt = db.get(Sequel.function(function_name(:rodauth_get_salt), id))
|
|
254
|
+
hash = BCrypt::Engine.hash_secret(password, salt)
|
|
255
|
+
db.get(Sequel.function(function_name(:rodauth_valid_password_hash), id, hash))
|
|
256
|
+
end
|
|
257
|
+
else
|
|
258
|
+
# :nocov:
|
|
259
|
+
if hash = password_hash_ds.get(password_hash_column)
|
|
260
|
+
BCrypt::Password.new(hash) == password
|
|
261
|
+
end
|
|
262
|
+
# :nocov:
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def update_session
|
|
269
|
+
clear_session
|
|
270
|
+
session[session_key] = account_session_value
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Return a string for the parameter name. This will be an empty
|
|
274
|
+
# string if the parameter doesn't exist.
|
|
275
|
+
def param(key)
|
|
276
|
+
param_or_nil(key).to_s
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Return a string for the parameter name, or nil if there is no
|
|
280
|
+
# parameter with that name.
|
|
281
|
+
def param_or_nil(key)
|
|
282
|
+
value = request.params[key]
|
|
283
|
+
value.to_s unless value.nil?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def redirect(path)
|
|
287
|
+
request.redirect(path)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def transaction(opts={}, &block)
|
|
291
|
+
db.transaction(opts, &block)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if RUBY_VERSION >= '1.9'
|
|
295
|
+
def random_key
|
|
296
|
+
SecureRandom.urlsafe_base64(32)
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
# :nocov:
|
|
300
|
+
def random_key
|
|
301
|
+
SecureRandom.hex(32)
|
|
302
|
+
end
|
|
303
|
+
# :nocov:
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def timing_safe_eql?(provided, actual)
|
|
307
|
+
provided = provided.to_s
|
|
308
|
+
Rack::Utils.secure_compare(provided.ljust(actual.length), actual) && provided.length == actual.length
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def require_account
|
|
312
|
+
require_authentication
|
|
313
|
+
require_account_session
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def require_account_session
|
|
317
|
+
unless account_from_session
|
|
318
|
+
clear_session
|
|
319
|
+
login_required
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def catch_error(&block)
|
|
324
|
+
catch(:rodauth_error, &block)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def throw_error(field, error)
|
|
328
|
+
set_field_error(field, error)
|
|
329
|
+
throw :rodauth_error
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def use_date_arithmetic?
|
|
333
|
+
set_deadline_values?
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def set_deadline_values?
|
|
337
|
+
db.database_type == :mysql
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def use_database_authentication_functions?
|
|
341
|
+
case db.database_type
|
|
342
|
+
when :postgres, :mysql, :mssql
|
|
343
|
+
true
|
|
344
|
+
else
|
|
345
|
+
# :nocov:
|
|
346
|
+
false
|
|
347
|
+
# :nocov:
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def function_name(name)
|
|
352
|
+
if db.database_type == :mssql
|
|
353
|
+
# :nocov:
|
|
354
|
+
"dbo.#{name}"
|
|
355
|
+
# :nocov:
|
|
356
|
+
else
|
|
357
|
+
name
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def _account_from_login(login)
|
|
362
|
+
ds = db[accounts_table].where(login_column=>login)
|
|
363
|
+
ds = ds.select(*account_select) if account_select
|
|
364
|
+
ds = ds.where(account_status_column=>[account_unverified_status_value, account_open_status_value]) unless skip_status_checks?
|
|
365
|
+
ds.first
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def _account_from_session
|
|
369
|
+
ds = account_ds(session_value)
|
|
370
|
+
ds = ds.where(account_session_status_filter) unless skip_status_checks?
|
|
371
|
+
ds.first
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def account_session_status_filter
|
|
375
|
+
{account_status_column=>account_open_status_value}
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def template_path(page)
|
|
379
|
+
File.join(File.dirname(__FILE__), '../../../templates', "#{page}.str")
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def account_ds(id=account_id)
|
|
383
|
+
raise ArgumentError, "invalid account id passed to account_ds" unless id
|
|
384
|
+
ds = db[accounts_table].where(account_id_column=>id)
|
|
385
|
+
ds = ds.select(*account_select) if account_select
|
|
386
|
+
ds
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def password_hash_ds
|
|
390
|
+
db[password_hash_table].where(password_hash_id_column=>account_id)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# This is needed for jdbc/sqlite, which returns timestamp columns as strings
|
|
394
|
+
def convert_timestamp(timestamp)
|
|
395
|
+
timestamp = db.to_application_timestamp(timestamp) if timestamp.is_a?(String)
|
|
396
|
+
timestamp
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# This is used to avoid race conditions when using the pattern of inserting when
|
|
400
|
+
# an update affects no rows. In such cases, if a row is inserted between the
|
|
401
|
+
# update and the insert, the insert will fail with a uniqueness error, but
|
|
402
|
+
# retrying will work. It is possible for it to fail again, but only if the row
|
|
403
|
+
# is deleted before the update and readded before the insert, which is very
|
|
404
|
+
# unlikely to happen. In such cases, raising an exception is acceptable.
|
|
405
|
+
def retry_on_uniqueness_violation(&block)
|
|
406
|
+
if raises_uniqueness_violation?(&block)
|
|
407
|
+
yield
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# In cases where retrying on uniqueness violations cannot work, this will detect
|
|
412
|
+
# whether a uniqueness violation is raised by the block and return the exception if so.
|
|
413
|
+
# This method should be used if you don't care about the exception itself.
|
|
414
|
+
def raises_uniqueness_violation?(&block)
|
|
415
|
+
transaction(:savepoint=>:only, &block)
|
|
416
|
+
false
|
|
417
|
+
rescue unique_constraint_violation_class => e
|
|
418
|
+
e
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Work around jdbc/sqlite issue where it only raises ConstraintViolation and not
|
|
422
|
+
# UniqueConstraintViolation.
|
|
423
|
+
def unique_constraint_violation_class
|
|
424
|
+
if db.adapter_scheme == :jdbc && db.database_type == :sqlite
|
|
425
|
+
# :nocov:
|
|
426
|
+
Sequel::ConstraintViolation
|
|
427
|
+
# :nocov:
|
|
428
|
+
else
|
|
429
|
+
Sequel::UniqueConstraintViolation
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# If you would like to operate/reraise the exception, this alias makes more sense.
|
|
434
|
+
alias raised_uniqueness_violation raises_uniqueness_violation?
|
|
435
|
+
|
|
436
|
+
# If you just want to ignore uniqueness violations, this alias makes more sense.
|
|
437
|
+
alias ignore_uniqueness_violation raises_uniqueness_violation?
|
|
438
|
+
|
|
439
|
+
# This is needed on MySQL, which doesn't support non constant defaults other than
|
|
440
|
+
# CURRENT_TIMESTAMP.
|
|
441
|
+
def set_deadline_value(hash, column, interval)
|
|
442
|
+
if set_deadline_values?
|
|
443
|
+
# :nocov:
|
|
444
|
+
hash[column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, interval)
|
|
445
|
+
# :nocov:
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def set_session_value(key, value)
|
|
450
|
+
session[key] = value
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def update_hash_ds(hash, ds, values)
|
|
454
|
+
num = ds.update(values)
|
|
455
|
+
if num == 1
|
|
456
|
+
values.each do |k, v|
|
|
457
|
+
account[k] = v == Sequel::CURRENT_TIMESTAMP ? Time.now : v
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
num
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def update_account(values, ds=account_ds)
|
|
464
|
+
update_hash_ds(account, ds, values)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def _view(meth, page)
|
|
468
|
+
auth = self
|
|
469
|
+
auth_template_path = template_path(page)
|
|
470
|
+
scope.instance_exec do
|
|
471
|
+
template_opts = find_template(parse_template_opts(page, :locals=>{:rodauth=>auth}))
|
|
472
|
+
unless File.file?(template_path(template_opts))
|
|
473
|
+
template_opts[:path] = auth_template_path
|
|
474
|
+
end
|
|
475
|
+
send(meth, template_opts)
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|