rodauth 1.22.0 → 2.3.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 +190 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +210 -80
- data/doc/account_expiration.rdoc +12 -26
- data/doc/active_sessions.rdoc +49 -0
- data/doc/audit_logging.rdoc +44 -0
- data/doc/base.rdoc +75 -128
- data/doc/change_login.rdoc +7 -14
- data/doc/change_password.rdoc +9 -13
- data/doc/change_password_notify.rdoc +2 -2
- data/doc/close_account.rdoc +9 -16
- data/doc/confirm_password.rdoc +12 -5
- data/doc/create_account.rdoc +11 -22
- data/doc/disallow_password_reuse.rdoc +6 -13
- data/doc/email_auth.rdoc +15 -14
- data/doc/email_base.rdoc +6 -15
- data/doc/guides/admin_activation.rdoc +46 -0
- data/doc/guides/already_authenticated.rdoc +10 -0
- data/doc/guides/alternative_login.rdoc +46 -0
- data/doc/guides/create_account_programmatically.rdoc +38 -0
- data/doc/guides/delay_password.rdoc +25 -0
- data/doc/guides/email_only.rdoc +16 -0
- data/doc/guides/i18n.rdoc +26 -0
- data/doc/{internals.rdoc → guides/internals.rdoc} +0 -0
- data/doc/guides/links.rdoc +12 -0
- data/doc/guides/login_return.rdoc +37 -0
- data/doc/guides/password_column.rdoc +25 -0
- data/doc/guides/password_confirmation.rdoc +37 -0
- data/doc/guides/password_requirements.rdoc +30 -0
- data/doc/guides/paths.rdoc +36 -0
- data/doc/guides/query_params.rdoc +9 -0
- data/doc/guides/redirects.rdoc +17 -0
- data/doc/guides/registration_field.rdoc +68 -0
- data/doc/guides/require_mfa.rdoc +30 -0
- data/doc/guides/reset_password_autologin.rdoc +21 -0
- data/doc/guides/status_column.rdoc +28 -0
- data/doc/guides/totp_or_recovery.rdoc +16 -0
- data/doc/http_basic_auth.rdoc +10 -1
- data/doc/jwt.rdoc +22 -22
- data/doc/jwt_cors.rdoc +2 -3
- data/doc/jwt_refresh.rdoc +23 -8
- data/doc/lockout.rdoc +17 -15
- data/doc/login.rdoc +17 -2
- data/doc/login_password_requirements_base.rdoc +18 -37
- data/doc/logout.rdoc +2 -2
- data/doc/otp.rdoc +25 -19
- data/doc/password_complexity.rdoc +10 -26
- data/doc/password_expiration.rdoc +11 -25
- data/doc/password_grace_period.rdoc +16 -2
- data/doc/recovery_codes.rdoc +18 -12
- data/doc/release_notes/1.23.0.txt +32 -0
- data/doc/release_notes/2.0.0.txt +361 -0
- data/doc/release_notes/2.1.0.txt +31 -0
- data/doc/release_notes/2.2.0.txt +39 -0
- data/doc/release_notes/2.3.0.txt +37 -0
- data/doc/remember.rdoc +40 -64
- data/doc/reset_password.rdoc +12 -9
- data/doc/session_expiration.rdoc +1 -0
- data/doc/single_session.rdoc +16 -25
- data/doc/sms_codes.rdoc +24 -14
- data/doc/two_factor_base.rdoc +60 -22
- data/doc/verify_account.rdoc +14 -12
- data/doc/verify_account_grace_period.rdoc +6 -2
- data/doc/verify_login_change.rdoc +9 -8
- data/doc/webauthn.rdoc +115 -0
- data/doc/webauthn_login.rdoc +15 -0
- data/doc/webauthn_verify_account.rdoc +9 -0
- data/javascript/webauthn_auth.js +45 -0
- data/javascript/webauthn_setup.js +35 -0
- data/lib/roda/plugins/rodauth.rb +1 -1
- data/lib/rodauth.rb +36 -28
- data/lib/rodauth/features/account_expiration.rb +5 -5
- data/lib/rodauth/features/active_sessions.rb +158 -0
- data/lib/rodauth/features/audit_logging.rb +98 -0
- data/lib/rodauth/features/base.rb +144 -43
- data/lib/rodauth/features/change_password_notify.rb +2 -2
- data/lib/rodauth/features/close_account.rb +8 -6
- data/lib/rodauth/features/confirm_password.rb +40 -2
- data/lib/rodauth/features/create_account.rb +8 -13
- data/lib/rodauth/features/disallow_common_passwords.rb +1 -1
- data/lib/rodauth/features/disallow_password_reuse.rb +1 -1
- data/lib/rodauth/features/email_auth.rb +31 -30
- data/lib/rodauth/features/email_base.rb +9 -4
- data/lib/rodauth/features/http_basic_auth.rb +55 -35
- data/lib/rodauth/features/jwt.rb +63 -16
- data/lib/rodauth/features/jwt_cors.rb +15 -15
- data/lib/rodauth/features/jwt_refresh.rb +42 -13
- data/lib/rodauth/features/lockout.rb +12 -14
- data/lib/rodauth/features/login.rb +64 -15
- data/lib/rodauth/features/login_password_requirements_base.rb +13 -8
- data/lib/rodauth/features/otp.rb +77 -80
- data/lib/rodauth/features/password_complexity.rb +8 -13
- data/lib/rodauth/features/password_expiration.rb +2 -2
- data/lib/rodauth/features/password_grace_period.rb +17 -10
- data/lib/rodauth/features/recovery_codes.rb +49 -53
- data/lib/rodauth/features/remember.rb +11 -27
- data/lib/rodauth/features/reset_password.rb +26 -26
- data/lib/rodauth/features/session_expiration.rb +7 -10
- data/lib/rodauth/features/single_session.rb +8 -6
- data/lib/rodauth/features/sms_codes.rb +62 -72
- data/lib/rodauth/features/two_factor_base.rb +134 -30
- data/lib/rodauth/features/verify_account.rb +29 -21
- data/lib/rodauth/features/verify_account_grace_period.rb +18 -9
- data/lib/rodauth/features/verify_login_change.rb +12 -11
- data/lib/rodauth/features/webauthn.rb +505 -0
- data/lib/rodauth/features/webauthn_login.rb +70 -0
- data/lib/rodauth/features/webauthn_verify_account.rb +46 -0
- data/lib/rodauth/migrations.rb +16 -5
- data/lib/rodauth/version.rb +2 -2
- data/templates/button.str +1 -3
- data/templates/change-login.str +1 -2
- data/templates/change-password.str +3 -5
- data/templates/close-account.str +2 -2
- data/templates/confirm-password.str +1 -1
- data/templates/create-account.str +1 -1
- data/templates/email-auth-request-form.str +2 -3
- data/templates/email-auth.str +1 -1
- data/templates/global-logout-field.str +6 -0
- data/templates/login-confirm-field.str +2 -4
- data/templates/login-display.str +3 -2
- data/templates/login-field.str +2 -4
- data/templates/login-form-footer.str +6 -0
- data/templates/login-form.str +7 -0
- data/templates/login.str +1 -9
- data/templates/logout.str +1 -1
- data/templates/multi-phase-login.str +3 -0
- data/templates/otp-auth-code-field.str +5 -3
- data/templates/otp-auth.str +1 -1
- data/templates/otp-disable.str +1 -1
- data/templates/otp-setup.str +3 -3
- data/templates/password-confirm-field.str +2 -4
- data/templates/password-field.str +2 -4
- data/templates/recovery-auth.str +3 -6
- data/templates/recovery-codes.str +1 -1
- data/templates/remember.str +15 -20
- data/templates/reset-password-request.str +3 -3
- data/templates/reset-password.str +1 -2
- data/templates/sms-auth.str +1 -1
- data/templates/sms-code-field.str +5 -3
- data/templates/sms-confirm.str +1 -2
- data/templates/sms-disable.str +1 -2
- data/templates/sms-request.str +1 -1
- data/templates/sms-setup.str +6 -4
- data/templates/two-factor-auth.str +5 -0
- data/templates/two-factor-disable.str +6 -0
- data/templates/two-factor-manage.str +16 -0
- data/templates/unlock-account-request.str +4 -4
- data/templates/unlock-account.str +1 -1
- data/templates/verify-account-resend.str +3 -3
- data/templates/verify-account.str +1 -2
- data/templates/verify-login-change.str +1 -1
- data/templates/webauthn-auth.str +11 -0
- data/templates/webauthn-remove.str +14 -0
- data/templates/webauthn-setup.str +12 -0
- metadata +94 -54
- data/Rakefile +0 -179
- data/doc/verify_change_login.rdoc +0 -11
- data/lib/rodauth/features/verify_change_login.rb +0 -20
- data/spec/account_expiration_spec.rb +0 -225
- data/spec/all.rb +0 -1
- data/spec/change_login_spec.rb +0 -156
- data/spec/change_password_notify_spec.rb +0 -33
- data/spec/change_password_spec.rb +0 -202
- data/spec/close_account_spec.rb +0 -162
- data/spec/confirm_password_spec.rb +0 -70
- data/spec/create_account_spec.rb +0 -127
- data/spec/disallow_common_passwords_spec.rb +0 -93
- data/spec/disallow_password_reuse_spec.rb +0 -179
- data/spec/email_auth_spec.rb +0 -285
- data/spec/http_basic_auth_spec.rb +0 -143
- data/spec/jwt_cors_spec.rb +0 -57
- data/spec/jwt_refresh_spec.rb +0 -256
- data/spec/jwt_spec.rb +0 -235
- data/spec/lockout_spec.rb +0 -250
- data/spec/login_spec.rb +0 -328
- data/spec/migrate/001_tables.rb +0 -184
- data/spec/migrate/002_account_password_hash_column.rb +0 -11
- data/spec/migrate_password/001_tables.rb +0 -73
- data/spec/migrate_travis/001_tables.rb +0 -141
- data/spec/password_complexity_spec.rb +0 -109
- data/spec/password_expiration_spec.rb +0 -244
- data/spec/password_grace_period_spec.rb +0 -93
- data/spec/remember_spec.rb +0 -451
- data/spec/reset_password_spec.rb +0 -229
- data/spec/rodauth_spec.rb +0 -343
- data/spec/session_expiration_spec.rb +0 -58
- data/spec/single_session_spec.rb +0 -127
- data/spec/spec_helper.rb +0 -327
- data/spec/two_factor_spec.rb +0 -1462
- data/spec/update_password_hash_spec.rb +0 -40
- data/spec/verify_account_grace_period_spec.rb +0 -171
- data/spec/verify_account_spec.rb +0 -240
- data/spec/verify_change_login_spec.rb +0 -46
- data/spec/verify_login_change_spec.rb +0 -232
- data/spec/views/layout-other.str +0 -11
- data/spec/views/layout.str +0 -11
- data/spec/views/login.str +0 -21
data/lib/roda/plugins/rodauth.rb
CHANGED
data/lib/rodauth.rb
CHANGED
|
@@ -14,15 +14,15 @@ module Rodauth
|
|
|
14
14
|
require 'tilt/string'
|
|
15
15
|
app.plugin :render
|
|
16
16
|
|
|
17
|
-
case opts.fetch(:csrf, app.opts[:
|
|
17
|
+
case opts.fetch(:csrf, app.opts[:rodauth_csrf])
|
|
18
18
|
when false
|
|
19
19
|
# nothing
|
|
20
|
-
when :
|
|
21
|
-
app.plugin :route_csrf
|
|
22
|
-
else
|
|
20
|
+
when :rack_csrf
|
|
23
21
|
# :nocov:
|
|
24
22
|
app.plugin :csrf
|
|
25
23
|
# :nocov:
|
|
24
|
+
else
|
|
25
|
+
app.plugin :route_csrf
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
app.plugin :flash unless opts[:flash] == false
|
|
@@ -31,8 +31,14 @@ module Rodauth
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.configure(app, opts={}, &block)
|
|
34
|
-
app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
|
|
35
|
-
app.opts[:rodauth_csrf] = opts.fetch(:csrf, app.opts[:
|
|
34
|
+
json_opt = app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
|
|
35
|
+
csrf = app.opts[:rodauth_csrf] = opts.fetch(:csrf, app.opts[:rodauth_csrf])
|
|
36
|
+
app.opts[:rodauth_route_csrf] = case csrf
|
|
37
|
+
when false, :rack_csrf
|
|
38
|
+
false
|
|
39
|
+
else
|
|
40
|
+
json_opt != :only
|
|
41
|
+
end
|
|
36
42
|
auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= Class.new(Auth)
|
|
37
43
|
if !auth_class.roda_class
|
|
38
44
|
auth_class.roda_class = app
|
|
@@ -72,8 +78,7 @@ module Rodauth
|
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
def def_auth_value_method(meth, priv)
|
|
75
|
-
define_method(meth) do
|
|
76
|
-
v = v.first
|
|
81
|
+
define_method(meth) do |v=nil, &block|
|
|
77
82
|
block ||= proc{v}
|
|
78
83
|
@auth.send(:define_method, meth, &block)
|
|
79
84
|
@auth.send(:private, meth) if priv
|
|
@@ -101,23 +106,20 @@ module Rodauth
|
|
|
101
106
|
attr_accessor :configuration
|
|
102
107
|
|
|
103
108
|
def route(name=feature_name, default=name.to_s.tr('_', '-'), &block)
|
|
104
|
-
|
|
109
|
+
route_meth = :"#{name}_route"
|
|
110
|
+
auth_value_method route_meth, default
|
|
111
|
+
|
|
112
|
+
define_method(:"#{name}_path"){|opts={}| route_path(send(route_meth), opts)}
|
|
113
|
+
define_method(:"#{name}_url"){|opts={}| route_url(send(route_meth), opts)}
|
|
105
114
|
|
|
106
115
|
handle_meth = :"handle_#{name}"
|
|
107
116
|
internal_handle_meth = :"_#{handle_meth}"
|
|
108
|
-
route_meth = :"#{name}_route"
|
|
109
117
|
before route_meth
|
|
110
|
-
|
|
111
|
-
unless block.arity == 1
|
|
112
|
-
# :nocov:
|
|
113
|
-
b = block
|
|
114
|
-
block = lambda{|r| instance_exec(r, &b)}
|
|
115
|
-
# :nocov:
|
|
116
|
-
end
|
|
117
118
|
define_method(internal_handle_meth, &block)
|
|
118
119
|
|
|
119
120
|
define_method(handle_meth) do
|
|
120
121
|
request.is send(route_meth) do
|
|
122
|
+
check_csrf if check_csrf?
|
|
121
123
|
before_rodauth
|
|
122
124
|
send(internal_handle_meth, request)
|
|
123
125
|
end
|
|
@@ -135,7 +137,9 @@ module Rodauth
|
|
|
135
137
|
feature.module_eval(&block)
|
|
136
138
|
configuration.def_configuration_methods(feature)
|
|
137
139
|
|
|
140
|
+
# :nocov:
|
|
138
141
|
if constant
|
|
142
|
+
# :nocov:
|
|
139
143
|
Rodauth.const_set(constant, feature)
|
|
140
144
|
Rodauth::FeatureConfiguration.const_set(constant, configuration)
|
|
141
145
|
end
|
|
@@ -177,8 +181,10 @@ module Rodauth
|
|
|
177
181
|
|
|
178
182
|
def view(page, title, name=feature_name)
|
|
179
183
|
meth = :"#{name}_view"
|
|
184
|
+
title_meth = :"#{name}_page_title"
|
|
185
|
+
translatable_method(title_meth, title)
|
|
180
186
|
define_method(meth) do
|
|
181
|
-
view(page,
|
|
187
|
+
view(page, send(title_meth))
|
|
182
188
|
end
|
|
183
189
|
auth_methods meth
|
|
184
190
|
end
|
|
@@ -187,6 +193,7 @@ module Rodauth
|
|
|
187
193
|
define_method(:loaded_templates) do
|
|
188
194
|
super().concat(v)
|
|
189
195
|
end
|
|
196
|
+
private :loaded_templates
|
|
190
197
|
end
|
|
191
198
|
|
|
192
199
|
def depends(*deps)
|
|
@@ -194,10 +201,9 @@ module Rodauth
|
|
|
194
201
|
end
|
|
195
202
|
|
|
196
203
|
%w'after before'.each do |hook|
|
|
197
|
-
define_method(hook) do
|
|
198
|
-
name = args[0] || feature_name
|
|
204
|
+
define_method(hook) do |name=feature_name|
|
|
199
205
|
meth = "#{hook}_#{name}"
|
|
200
|
-
class_eval("def #{meth}; super if defined?(super); _#{meth} end", __FILE__, __LINE__)
|
|
206
|
+
class_eval("def #{meth}; super if defined?(super); _#{meth}; hook_action(:#{hook}, :#{name}); nil end", __FILE__, __LINE__)
|
|
201
207
|
class_eval("def _#{meth}; nil end", __FILE__, __LINE__)
|
|
202
208
|
private meth, :"_#{meth}"
|
|
203
209
|
auth_private_methods(meth)
|
|
@@ -218,6 +224,11 @@ module Rodauth
|
|
|
218
224
|
auth_value_methods(meth)
|
|
219
225
|
end
|
|
220
226
|
|
|
227
|
+
def translatable_method(meth, value)
|
|
228
|
+
define_method(meth){translate(meth, value)}
|
|
229
|
+
auth_value_methods(meth)
|
|
230
|
+
end
|
|
231
|
+
|
|
221
232
|
def auth_cached_method(meth, iv=:"@#{meth}")
|
|
222
233
|
umeth = :"_#{meth}"
|
|
223
234
|
define_method(meth) do
|
|
@@ -231,9 +242,8 @@ module Rodauth
|
|
|
231
242
|
end
|
|
232
243
|
|
|
233
244
|
[:notice_flash, :error_flash, :button].each do |meth|
|
|
234
|
-
define_method(meth) do |v,
|
|
235
|
-
name
|
|
236
|
-
auth_value_method(:"#{name}_#{meth}", v)
|
|
245
|
+
define_method(meth) do |v, name=feature_name|
|
|
246
|
+
translatable_method(:"#{name}_#{meth}", v)
|
|
237
247
|
end
|
|
238
248
|
end
|
|
239
249
|
end
|
|
@@ -328,10 +338,8 @@ module Rodauth
|
|
|
328
338
|
end
|
|
329
339
|
|
|
330
340
|
def freeze
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
opts[:rodauths].freeze
|
|
334
|
-
end
|
|
341
|
+
opts[:rodauths].each_value(&:freeze)
|
|
342
|
+
opts[:rodauths].freeze
|
|
335
343
|
super
|
|
336
344
|
end
|
|
337
345
|
end
|
|
@@ -69,6 +69,11 @@ module Rodauth
|
|
|
69
69
|
update_last_login
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def update_session
|
|
73
|
+
check_account_expiration
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
private
|
|
73
78
|
|
|
74
79
|
def before_reset_password
|
|
@@ -96,11 +101,6 @@ module Rodauth
|
|
|
96
101
|
account_activity_ds(account_id).delete
|
|
97
102
|
end
|
|
98
103
|
|
|
99
|
-
def update_session
|
|
100
|
-
check_account_expiration
|
|
101
|
-
super
|
|
102
|
-
end
|
|
103
|
-
|
|
104
104
|
def account_activity_ds(account_id)
|
|
105
105
|
db[account_activity_table].
|
|
106
106
|
where(account_activity_id_column=>account_id)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Feature.define(:active_sessions, :ActiveSessions) do
|
|
5
|
+
depends :logout
|
|
6
|
+
|
|
7
|
+
error_flash 'This session has been logged out'
|
|
8
|
+
redirect
|
|
9
|
+
|
|
10
|
+
session_key :session_id_session_key, :active_session_id
|
|
11
|
+
auth_value_method :active_sessions_account_id_column, :account_id
|
|
12
|
+
auth_value_method :active_sessions_created_at_column, :created_at
|
|
13
|
+
auth_value_method :active_sessions_last_use_column, :last_use
|
|
14
|
+
auth_value_method :active_sessions_session_id_column, :session_id
|
|
15
|
+
auth_value_method :active_sessions_table, :account_active_session_keys
|
|
16
|
+
translatable_method :global_logout_label, 'Logout all Logged In Sessons?'
|
|
17
|
+
auth_value_method :global_logout_param, 'global_logout'
|
|
18
|
+
auth_value_method :inactive_session_error_status, 401
|
|
19
|
+
auth_value_method :session_inactivity_deadline, 86400
|
|
20
|
+
auth_value_method(:session_lifetime_deadline, 86400*30)
|
|
21
|
+
|
|
22
|
+
auth_methods(
|
|
23
|
+
:add_active_session,
|
|
24
|
+
:currently_active_session?,
|
|
25
|
+
:handle_duplicate_active_session_id,
|
|
26
|
+
:no_longer_active_session,
|
|
27
|
+
:remove_all_active_sessions,
|
|
28
|
+
:remove_current_session,
|
|
29
|
+
:remove_inactive_sessions,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def currently_active_session?
|
|
33
|
+
return false unless session_id = session[session_id_session_key]
|
|
34
|
+
|
|
35
|
+
remove_inactive_sessions
|
|
36
|
+
ds = active_sessions_ds.
|
|
37
|
+
where(active_sessions_session_id_column => compute_hmac(session_id))
|
|
38
|
+
|
|
39
|
+
if session_inactivity_deadline
|
|
40
|
+
ds.update(active_sessions_last_use_column => Sequel::CURRENT_TIMESTAMP) == 1
|
|
41
|
+
else
|
|
42
|
+
ds.count == 1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_active_session
|
|
47
|
+
if logged_in? && !currently_active_session?
|
|
48
|
+
no_longer_active_session
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def no_longer_active_session
|
|
53
|
+
clear_session
|
|
54
|
+
set_redirect_error_status inactive_session_error_status
|
|
55
|
+
set_redirect_error_flash active_sessions_error_flash
|
|
56
|
+
redirect active_sessions_redirect
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_active_session
|
|
60
|
+
key = random_key
|
|
61
|
+
set_session_value(session_id_session_key, key)
|
|
62
|
+
if e = raises_uniqueness_violation? do
|
|
63
|
+
active_sessions_ds.insert(active_sessions_account_id_column => session_value, active_sessions_session_id_column => compute_hmac(key))
|
|
64
|
+
end
|
|
65
|
+
handle_duplicate_active_session_id(e)
|
|
66
|
+
end
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def handle_duplicate_active_session_id(_e)
|
|
71
|
+
# Do nothing by default as session is already tracked. This will result in
|
|
72
|
+
# the current session and the existing session with the same id
|
|
73
|
+
# being tracked together, so that a logout of one will logout
|
|
74
|
+
# the other, and updating the last use on one will update the other,
|
|
75
|
+
# but this should be acceptable. However, this can be overridden if different
|
|
76
|
+
# behavior is desired.
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def remove_current_session
|
|
80
|
+
active_sessions_ds.where(active_sessions_session_id_column=>compute_hmac(session[session_id_session_key])).delete
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remove_all_active_sessions
|
|
84
|
+
active_sessions_ds.delete
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def remove_inactive_sessions
|
|
88
|
+
if cond = inactive_session_cond
|
|
89
|
+
active_sessions_ds.where(cond).delete
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def logout_additional_form_tags
|
|
94
|
+
super.to_s + render('global-logout-field')
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def update_session
|
|
98
|
+
super
|
|
99
|
+
add_active_session
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def after_refresh_token
|
|
105
|
+
super if defined?(super)
|
|
106
|
+
if prev_key = session[session_id_session_key]
|
|
107
|
+
key = random_key
|
|
108
|
+
set_session_value(session_id_session_key, key)
|
|
109
|
+
active_sessions_ds.
|
|
110
|
+
where(active_sessions_session_id_column => compute_hmac(prev_key)).
|
|
111
|
+
update(active_sessions_session_id_column => compute_hmac(key))
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def after_close_account
|
|
116
|
+
super if defined?(super)
|
|
117
|
+
remove_all_active_sessions
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def before_logout
|
|
121
|
+
if param_or_nil(global_logout_param)
|
|
122
|
+
remove_all_active_sessions
|
|
123
|
+
else
|
|
124
|
+
remove_current_session
|
|
125
|
+
end
|
|
126
|
+
super
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def session_inactivity_deadline_condition
|
|
130
|
+
if deadline = session_inactivity_deadline
|
|
131
|
+
Sequel[active_sessions_last_use_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def session_lifetime_deadline_condition
|
|
136
|
+
if deadline = session_lifetime_deadline
|
|
137
|
+
Sequel[active_sessions_created_at_column] < Sequel.date_sub(Sequel::CURRENT_TIMESTAMP, seconds: deadline)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def inactive_session_cond
|
|
142
|
+
cond = session_inactivity_deadline_condition
|
|
143
|
+
cond2 = session_lifetime_deadline_condition
|
|
144
|
+
return false unless cond || cond2
|
|
145
|
+
Sequel.|(*[cond, cond2].compact)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def active_sessions_ds
|
|
149
|
+
db[active_sessions_table].
|
|
150
|
+
where(active_sessions_account_id_column=>session_value)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def use_date_arithmetic?
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
module Rodauth
|
|
4
|
+
Feature.define(:audit_logging, :AuditLogging) do
|
|
5
|
+
auth_value_method :audit_logging_account_id_column, :account_id
|
|
6
|
+
auth_value_method :audit_logging_message_column, :message
|
|
7
|
+
auth_value_method :audit_logging_metadata_column, :metadata
|
|
8
|
+
auth_value_method :audit_logging_table, :account_authentication_audit_logs
|
|
9
|
+
auth_value_method :audit_log_metadata_default, nil
|
|
10
|
+
|
|
11
|
+
auth_methods(
|
|
12
|
+
:add_audit_log,
|
|
13
|
+
:audit_log_insert_hash,
|
|
14
|
+
:audit_log_message,
|
|
15
|
+
:audit_log_message_default,
|
|
16
|
+
:audit_log_metadata,
|
|
17
|
+
:serialize_audit_log_metadata,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
configuration_module_eval do
|
|
21
|
+
[:audit_log_message_for, :audit_log_metadata_for].each do |method|
|
|
22
|
+
define_method(method) do |action, value=nil, &block|
|
|
23
|
+
block ||= proc{value}
|
|
24
|
+
meth = :"#{method}_#{action}"
|
|
25
|
+
@auth.send(:define_method, meth, &block)
|
|
26
|
+
@auth.send(:private, meth)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def hook_action(hook_type, action)
|
|
32
|
+
super
|
|
33
|
+
# In after_logout, session is already cleared, so use before_logout in that case
|
|
34
|
+
if (hook_type == :after || action == :logout) && (id = account ? account_id : session_value)
|
|
35
|
+
add_audit_log(id, action)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add_audit_log(account_id, action)
|
|
40
|
+
if hash = audit_log_insert_hash(account_id, action)
|
|
41
|
+
audit_log_ds.insert(hash)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def audit_log_insert_hash(account_id, action)
|
|
46
|
+
if message = audit_log_message(action)
|
|
47
|
+
{
|
|
48
|
+
audit_logging_account_id_column => account_id,
|
|
49
|
+
audit_logging_message_column => message,
|
|
50
|
+
audit_logging_metadata_column => serialize_audit_log_metadata(audit_log_metadata(action))
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def serialize_audit_log_metadata(metadata)
|
|
56
|
+
metadata.to_json unless metadata.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def audit_log_message_default(action)
|
|
60
|
+
action.to_s
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def audit_log_message(action)
|
|
64
|
+
meth = :"audit_log_message_for_#{action}"
|
|
65
|
+
if respond_to?(meth, true)
|
|
66
|
+
send(meth)
|
|
67
|
+
else
|
|
68
|
+
audit_log_message_default(action)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def audit_log_metadata(action)
|
|
73
|
+
meth = :"audit_log_metadata_for_#{action}"
|
|
74
|
+
if respond_to?(meth, true)
|
|
75
|
+
send(meth)
|
|
76
|
+
else
|
|
77
|
+
audit_log_metadata_default
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def audit_log_ds
|
|
84
|
+
ds = db[audit_logging_table]
|
|
85
|
+
# :nocov:
|
|
86
|
+
if db.database_type == :postgres
|
|
87
|
+
# :nocov:
|
|
88
|
+
# For PostgreSQL, use RETURNING NULL. This allows the feature
|
|
89
|
+
# to be used with INSERT but not SELECT permissions on the
|
|
90
|
+
# table, useful for audit logging where the database user
|
|
91
|
+
# the application is running as should not need to read the
|
|
92
|
+
# logs.
|
|
93
|
+
ds = ds.returning(nil)
|
|
94
|
+
end
|
|
95
|
+
ds
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -18,17 +18,19 @@ module Rodauth
|
|
|
18
18
|
auth_value_method :account_unverified_status_value, 1
|
|
19
19
|
auth_value_method :accounts_table, :accounts
|
|
20
20
|
auth_value_method :cache_templates, true
|
|
21
|
+
auth_value_method :check_csrf_block, nil
|
|
22
|
+
auth_value_method :check_csrf_opts, {}.freeze
|
|
21
23
|
auth_value_method :default_redirect, '/'
|
|
22
24
|
session_key :flash_error_key, :error
|
|
23
25
|
session_key :flash_notice_key, :notice
|
|
24
26
|
auth_value_method :hmac_secret, nil
|
|
25
|
-
|
|
26
|
-
auth_value_method :input_field_error_class, 'error'
|
|
27
|
-
auth_value_method :input_field_error_message_class, 'error_message'
|
|
27
|
+
translatable_method :input_field_label_suffix, ''
|
|
28
|
+
auth_value_method :input_field_error_class, 'error is-invalid'
|
|
29
|
+
auth_value_method :input_field_error_message_class, 'error_message invalid-feedback'
|
|
28
30
|
auth_value_method :invalid_field_error_status, 422
|
|
29
31
|
auth_value_method :invalid_key_error_status, 401
|
|
30
32
|
auth_value_method :invalid_password_error_status, 401
|
|
31
|
-
|
|
33
|
+
translatable_method :invalid_password_message, "invalid password"
|
|
32
34
|
auth_value_method :login_column, :email
|
|
33
35
|
auth_value_method :login_required_error_status, 401
|
|
34
36
|
auth_value_method :lockout_error_status, 403
|
|
@@ -36,30 +38,38 @@ module Rodauth
|
|
|
36
38
|
auth_value_method :password_hash_column, :password_hash
|
|
37
39
|
auth_value_method :password_hash_table, :account_password_hashes
|
|
38
40
|
auth_value_method :no_matching_login_error_status, 401
|
|
39
|
-
|
|
41
|
+
translatable_method :no_matching_login_message, "no matching login"
|
|
40
42
|
auth_value_method :login_param, 'login'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
auth_value_method :password_label, 'Password'
|
|
43
|
+
translatable_method :login_label, 'Login'
|
|
44
|
+
translatable_method :password_label, 'Password'
|
|
44
45
|
auth_value_method :password_param, 'password'
|
|
45
|
-
auth_value_method :modifications_require_password?, true
|
|
46
46
|
session_key :session_key, :account_id
|
|
47
|
+
session_key :authenticated_by_session_key, :authenticated_by
|
|
48
|
+
session_key :autologin_type_session_key, :autologin_type
|
|
47
49
|
auth_value_method :prefix, ''
|
|
48
50
|
auth_value_method :require_bcrypt?, true
|
|
49
|
-
auth_value_method :mark_input_fields_as_required?,
|
|
51
|
+
auth_value_method :mark_input_fields_as_required?, true
|
|
52
|
+
auth_value_method :mark_input_fields_with_autocomplete?, true
|
|
53
|
+
auth_value_method :mark_input_fields_with_inputmode?, true
|
|
50
54
|
auth_value_method :skip_status_checks?, true
|
|
51
|
-
auth_value_method :template_opts, {}
|
|
55
|
+
auth_value_method :template_opts, {}.freeze
|
|
52
56
|
auth_value_method :title_instance_variable, nil
|
|
53
57
|
auth_value_method :token_separator, "_"
|
|
54
58
|
auth_value_method :unmatched_field_error_status, 422
|
|
55
59
|
auth_value_method :unopen_account_error_status, 403
|
|
56
|
-
|
|
60
|
+
translatable_method :unverified_account_message, "unverified account, please verify account before logging in"
|
|
61
|
+
auth_value_method :default_field_attributes, ''
|
|
57
62
|
|
|
58
63
|
redirect(:require_login){"#{prefix}/login"}
|
|
59
64
|
|
|
60
65
|
auth_value_methods(
|
|
66
|
+
:base_url,
|
|
67
|
+
:check_csrf?,
|
|
61
68
|
:db,
|
|
62
|
-
:
|
|
69
|
+
:domain,
|
|
70
|
+
:login_input_type,
|
|
71
|
+
:login_uses_email?,
|
|
72
|
+
:modifications_require_password?,
|
|
63
73
|
:set_deadline_values?,
|
|
64
74
|
:use_date_arithmetic?,
|
|
65
75
|
:use_database_authentication_functions?,
|
|
@@ -71,9 +81,13 @@ module Rodauth
|
|
|
71
81
|
:account_session_value,
|
|
72
82
|
:already_logged_in,
|
|
73
83
|
:authenticated?,
|
|
84
|
+
:autocomplete_for_field?,
|
|
85
|
+
:check_csrf,
|
|
74
86
|
:clear_session,
|
|
75
87
|
:csrf_tag,
|
|
76
88
|
:function_name,
|
|
89
|
+
:hook_action,
|
|
90
|
+
:inputmode_for_field?,
|
|
77
91
|
:logged_in?,
|
|
78
92
|
:login_required,
|
|
79
93
|
:open_account?,
|
|
@@ -86,6 +100,7 @@ module Rodauth
|
|
|
86
100
|
:set_notice_now_flash,
|
|
87
101
|
:set_redirect_error_flash,
|
|
88
102
|
:set_title,
|
|
103
|
+
:translate,
|
|
89
104
|
:unverified_account_message,
|
|
90
105
|
:update_session
|
|
91
106
|
)
|
|
@@ -102,13 +117,6 @@ module Rodauth
|
|
|
102
117
|
def auth_class_eval(&block)
|
|
103
118
|
auth.class_eval(&block)
|
|
104
119
|
end
|
|
105
|
-
|
|
106
|
-
def account_model(model)
|
|
107
|
-
warn "account_model is deprecated, use db and accounts_table settings"
|
|
108
|
-
db model.db
|
|
109
|
-
accounts_table model.table_name
|
|
110
|
-
account_select model.dataset.opts[:select]
|
|
111
|
-
end
|
|
112
120
|
end
|
|
113
121
|
|
|
114
122
|
attr_reader :scope
|
|
@@ -168,13 +176,29 @@ module Rodauth
|
|
|
168
176
|
value = opts.fetch(:value){scope.h param(param)}
|
|
169
177
|
end
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
|
|
179
|
+
field_class = opts.fetch(:class, "form-control")
|
|
180
|
+
|
|
181
|
+
if autocomplete_for_field?(param) && opts[:autocomplete]
|
|
182
|
+
autocomplete = "autocomplete=\"#{opts[:autocomplete]}\""
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if inputmode_for_field?(param) && opts[:inputmode]
|
|
186
|
+
inputmode = "inputmode=\"#{opts[:inputmode]}\""
|
|
187
|
+
end
|
|
173
188
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"required=\"required\""
|
|
189
|
+
if mark_input_fields_as_required? && opts[:required] != false
|
|
190
|
+
required = "required=\"required\""
|
|
177
191
|
end
|
|
192
|
+
|
|
193
|
+
"<input #{opts[:attr]} #{autocomplete} #{inputmode} #{required} #{field_attributes(param)} #{field_error_attributes(param)} type=\"#{type}\" class=\"#{field_class}#{add_field_error_class(param)}\" name=\"#{param}\" id=\"#{id}\" value=\"#{value}\"/> #{formatted_field_error(param) unless opts[:skip_error_message]}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def autocomplete_for_field?(_param)
|
|
197
|
+
mark_input_fields_with_autocomplete?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def inputmode_for_field?(_param)
|
|
201
|
+
mark_input_fields_with_inputmode?
|
|
178
202
|
end
|
|
179
203
|
|
|
180
204
|
def field_attributes(field)
|
|
@@ -193,6 +217,15 @@ module Rodauth
|
|
|
193
217
|
end
|
|
194
218
|
end
|
|
195
219
|
|
|
220
|
+
def hook_action(_hook_type, _action)
|
|
221
|
+
# nothing by default
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def translate(_key, default)
|
|
225
|
+
# do not attempt to translate by default
|
|
226
|
+
default
|
|
227
|
+
end
|
|
228
|
+
|
|
196
229
|
# Return urlsafe base64 HMAC for data, assumes hmac_secret is set.
|
|
197
230
|
def compute_hmac(data)
|
|
198
231
|
s = [compute_raw_hmac(data)].pack('m').chomp!("=\n")
|
|
@@ -222,6 +255,10 @@ module Rodauth
|
|
|
222
255
|
Sequel::DATABASES.first
|
|
223
256
|
end
|
|
224
257
|
|
|
258
|
+
def password_field_autocomplete_value
|
|
259
|
+
@password_field_autocomplete_value || 'current-password'
|
|
260
|
+
end
|
|
261
|
+
|
|
225
262
|
# If the account_password_hash_column is set, the password hash is verified in
|
|
226
263
|
# ruby, it will not use a database function to do so, it will check the password
|
|
227
264
|
# hash using bcrypt.
|
|
@@ -237,6 +274,14 @@ module Rodauth
|
|
|
237
274
|
nil
|
|
238
275
|
end
|
|
239
276
|
|
|
277
|
+
def login_input_type
|
|
278
|
+
login_uses_email? ? 'email' : 'text'
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def login_uses_email?
|
|
282
|
+
login_column == :email
|
|
283
|
+
end
|
|
284
|
+
|
|
240
285
|
def clear_session
|
|
241
286
|
if scope.respond_to?(:clear_session)
|
|
242
287
|
scope.clear_session
|
|
@@ -293,6 +338,10 @@ module Rodauth
|
|
|
293
338
|
@account = _account_from_session
|
|
294
339
|
end
|
|
295
340
|
|
|
341
|
+
def check_csrf
|
|
342
|
+
scope.check_csrf!(check_csrf_opts, &check_csrf_block)
|
|
343
|
+
end
|
|
344
|
+
|
|
296
345
|
def csrf_tag(path=request.path)
|
|
297
346
|
return unless scope.respond_to?(:csrf_tag)
|
|
298
347
|
|
|
@@ -353,7 +402,25 @@ module Rodauth
|
|
|
353
402
|
|
|
354
403
|
def update_session
|
|
355
404
|
clear_session
|
|
356
|
-
|
|
405
|
+
set_session_value(session_key, account_session_value)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def authenticated_by
|
|
409
|
+
session[authenticated_by_session_key]
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def login_session(auth_type)
|
|
413
|
+
update_session
|
|
414
|
+
set_session_value(authenticated_by_session_key, [auth_type])
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def autologin_type
|
|
418
|
+
session[autologin_type_session_key]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def autologin_session(autologin_type)
|
|
422
|
+
login_session('autologin')
|
|
423
|
+
set_session_value(autologin_type_session_key, autologin_type)
|
|
357
424
|
end
|
|
358
425
|
|
|
359
426
|
# Return a string for the parameter name. This will be an empty
|
|
@@ -365,10 +432,30 @@ module Rodauth
|
|
|
365
432
|
# Return a string for the parameter name, or nil if there is no
|
|
366
433
|
# parameter with that name.
|
|
367
434
|
def param_or_nil(key)
|
|
368
|
-
value =
|
|
435
|
+
value = raw_param(key)
|
|
369
436
|
value.to_s unless value.nil?
|
|
370
437
|
end
|
|
371
438
|
|
|
439
|
+
def raw_param(key)
|
|
440
|
+
request.params[key]
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def base_url
|
|
444
|
+
request.base_url
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def domain
|
|
448
|
+
request.host
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def modifications_require_password?
|
|
452
|
+
has_password?
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def possible_authentication_methods
|
|
456
|
+
has_password? ? ['password'] : []
|
|
457
|
+
end
|
|
458
|
+
|
|
372
459
|
private
|
|
373
460
|
|
|
374
461
|
def convert_token_key(key)
|
|
@@ -387,22 +474,22 @@ module Rodauth
|
|
|
387
474
|
request.redirect(path)
|
|
388
475
|
end
|
|
389
476
|
|
|
477
|
+
def route_path(route, opts={})
|
|
478
|
+
path = "#{prefix}/#{route}"
|
|
479
|
+
path += "?#{Rack::Utils.build_nested_query(opts)}" unless opts.empty?
|
|
480
|
+
path
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def route_url(route, opts={})
|
|
484
|
+
"#{base_url}#{route_path(route, opts)}"
|
|
485
|
+
end
|
|
486
|
+
|
|
390
487
|
def transaction(opts={}, &block)
|
|
391
488
|
db.transaction(opts, &block)
|
|
392
489
|
end
|
|
393
490
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
SecureRandom.urlsafe_base64(32)
|
|
397
|
-
end
|
|
398
|
-
else
|
|
399
|
-
# :nocov:
|
|
400
|
-
def random_key
|
|
401
|
-
s = [SecureRandom.random_bytes(32)].pack('m').chomp!("=\n")
|
|
402
|
-
s.tr!('+/', '-_')
|
|
403
|
-
s
|
|
404
|
-
end
|
|
405
|
-
# :nocov:
|
|
491
|
+
def random_key
|
|
492
|
+
SecureRandom.urlsafe_base64(32)
|
|
406
493
|
end
|
|
407
494
|
|
|
408
495
|
def convert_session_key(key)
|
|
@@ -468,7 +555,11 @@ module Rodauth
|
|
|
468
555
|
end
|
|
469
556
|
|
|
470
557
|
def use_request_specific_csrf_tokens?
|
|
471
|
-
scope.opts[:
|
|
558
|
+
scope.opts[:rodauth_route_csrf] && scope.use_request_specific_csrf_tokens?
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def check_csrf?
|
|
562
|
+
scope.opts[:rodauth_route_csrf]
|
|
472
563
|
end
|
|
473
564
|
|
|
474
565
|
def function_name(name)
|
|
@@ -481,13 +572,19 @@ module Rodauth
|
|
|
481
572
|
end
|
|
482
573
|
end
|
|
483
574
|
|
|
575
|
+
def has_password?
|
|
576
|
+
return @has_password if defined?(@has_password)
|
|
577
|
+
return false unless account || session_value
|
|
578
|
+
@has_password = !!get_password_hash
|
|
579
|
+
end
|
|
580
|
+
|
|
484
581
|
# Get the password hash for the user. When using database authentication functions,
|
|
485
582
|
# note that only the salt is returned.
|
|
486
583
|
def get_password_hash
|
|
487
584
|
if account_password_hash_column
|
|
488
|
-
account[account_password_hash_column]
|
|
585
|
+
(account || account_from_session)[account_password_hash_column]
|
|
489
586
|
elsif use_database_authentication_functions?
|
|
490
|
-
db.get(Sequel.function(function_name(:rodauth_get_salt), account_id))
|
|
587
|
+
db.get(Sequel.function(function_name(:rodauth_get_salt), account ? account_id : session_value))
|
|
491
588
|
else
|
|
492
589
|
# :nocov:
|
|
493
590
|
password_hash_ds.get(password_hash_column)
|
|
@@ -540,7 +637,7 @@ module Rodauth
|
|
|
540
637
|
end
|
|
541
638
|
|
|
542
639
|
def password_hash_ds
|
|
543
|
-
db[password_hash_table].where(password_hash_id_column=>account_id)
|
|
640
|
+
db[password_hash_table].where(password_hash_id_column=>account ? account_id : session_value)
|
|
544
641
|
end
|
|
545
642
|
|
|
546
643
|
# This is needed for jdbc/sqlite, which returns timestamp columns as strings
|
|
@@ -607,6 +704,10 @@ module Rodauth
|
|
|
607
704
|
session[key] = value
|
|
608
705
|
end
|
|
609
706
|
|
|
707
|
+
def remove_session_value(key)
|
|
708
|
+
session.delete(key)
|
|
709
|
+
end
|
|
710
|
+
|
|
610
711
|
def update_hash_ds(hash, ds, values)
|
|
611
712
|
num = ds.update(values)
|
|
612
713
|
if num == 1
|