authenticate 0.3.1 → 0.3.2
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/.rubocop.yml +27 -0
- data/CHANGELOG.md +6 -0
- data/CONTRIBUTING.md +59 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +11 -11
- data/README.md +37 -4
- data/Rakefile +2 -4
- data/app/controllers/authenticate/passwords_controller.rb +3 -3
- data/app/controllers/authenticate/sessions_controller.rb +4 -4
- data/app/controllers/authenticate/users_controller.rb +5 -7
- data/app/mailers/authenticate_mailer.rb +6 -8
- data/authenticate.gemspec +8 -9
- data/lib/authenticate.rb +1 -1
- data/lib/authenticate/callbacks/authenticatable.rb +1 -2
- data/lib/authenticate/callbacks/brute_force.rb +1 -3
- data/lib/authenticate/callbacks/lifetimed.rb +2 -1
- data/lib/authenticate/callbacks/timeoutable.rb +3 -2
- data/lib/authenticate/callbacks/trackable.rb +1 -1
- data/lib/authenticate/configuration.rb +11 -7
- data/lib/authenticate/controller.rb +32 -23
- data/lib/authenticate/crypto/bcrypt.rb +3 -3
- data/lib/authenticate/debug.rb +7 -7
- data/lib/authenticate/engine.rb +4 -2
- data/lib/authenticate/lifecycle.rb +12 -22
- data/lib/authenticate/login_status.rb +4 -3
- data/lib/authenticate/model/brute_force.rb +4 -6
- data/lib/authenticate/model/db_password.rb +5 -14
- data/lib/authenticate/model/email.rb +7 -9
- data/lib/authenticate/model/lifetimed.rb +1 -2
- data/lib/authenticate/model/password_reset.rb +1 -3
- data/lib/authenticate/model/timeoutable.rb +14 -15
- data/lib/authenticate/model/trackable.rb +5 -4
- data/lib/authenticate/model/username.rb +3 -5
- data/lib/authenticate/modules.rb +37 -39
- data/lib/authenticate/session.rb +15 -23
- data/lib/authenticate/token.rb +3 -0
- data/lib/authenticate/user.rb +2 -6
- data/lib/authenticate/version.rb +1 -1
- data/lib/generators/authenticate/controllers/controllers_generator.rb +1 -2
- data/lib/generators/authenticate/helpers.rb +1 -2
- data/lib/generators/authenticate/install/install_generator.rb +31 -32
- data/lib/generators/authenticate/install/templates/authenticate.rb +0 -1
- data/lib/generators/authenticate/routes/routes_generator.rb +1 -2
- data/lib/generators/authenticate/views/USAGE +3 -2
- data/lib/generators/authenticate/views/views_generator.rb +1 -2
- data/spec/controllers/passwords_controller_spec.rb +5 -7
- data/spec/controllers/secured_controller_spec.rb +6 -6
- data/spec/controllers/sessions_controller_spec.rb +2 -2
- data/spec/controllers/users_controller_spec.rb +4 -4
- data/spec/features/brute_force_spec.rb +0 -2
- data/spec/features/max_session_lifetime_spec.rb +0 -1
- data/spec/features/password_reset_spec.rb +10 -19
- data/spec/features/password_update_spec.rb +0 -2
- data/spec/features/sign_out_spec.rb +0 -1
- data/spec/features/sign_up_spec.rb +0 -1
- data/spec/features/timeoutable_spec.rb +0 -1
- data/spec/model/brute_force_spec.rb +2 -3
- data/spec/model/configuration_spec.rb +2 -7
- data/spec/model/db_password_spec.rb +4 -6
- data/spec/model/email_spec.rb +1 -3
- data/spec/model/lifetimed_spec.rb +0 -3
- data/spec/model/modules_spec.rb +22 -0
- data/spec/model/password_reset_spec.rb +3 -10
- data/spec/model/session_spec.rb +4 -5
- data/spec/model/timeoutable_spec.rb +0 -1
- data/spec/model/token_spec.rb +1 -3
- data/spec/model/trackable_spec.rb +1 -2
- data/spec/model/user_spec.rb +0 -1
- data/spec/orm/active_record.rb +1 -1
- data/spec/spec_helper.rb +3 -11
- data/spec/support/controllers/controller_helpers.rb +1 -2
- data/spec/support/features/feature_helpers.rb +2 -4
- metadata +29 -26
@@ -1,6 +1,10 @@
|
|
1
|
+
# Authenticate
|
1
2
|
module Authenticate
|
3
|
+
#
|
4
|
+
# Configuration for Authenticate.
|
5
|
+
#
|
2
6
|
class Configuration
|
3
|
-
|
7
|
+
#
|
4
8
|
# ActiveRecord model class name that represents your user. Specify as a String.
|
5
9
|
#
|
6
10
|
# Defaults to '::User'.
|
@@ -224,12 +228,11 @@ module Authenticate
|
|
224
228
|
# @return [Boolean]
|
225
229
|
attr_accessor :debug
|
226
230
|
|
227
|
-
|
228
231
|
def initialize
|
229
232
|
# Defaults
|
230
233
|
@debug = false
|
231
234
|
@cookie_name = 'authenticate_session_token'
|
232
|
-
@cookie_expiration =
|
235
|
+
@cookie_expiration = -> { 1.year.from_now.utc }
|
233
236
|
@cookie_domain = nil
|
234
237
|
@cookie_path = '/'
|
235
238
|
@secure_cookie = false
|
@@ -286,10 +289,12 @@ module Authenticate
|
|
286
289
|
modules << :brute_force if @max_consecutive_bad_logins_allowed
|
287
290
|
modules
|
288
291
|
end
|
289
|
-
|
290
292
|
end # end of Configuration class
|
291
|
-
|
292
|
-
|
293
|
+
#
|
294
|
+
# Access to Authenticate's configuration, e.g.:
|
295
|
+
#
|
296
|
+
# puts Authenticate.configuration.redirect_url
|
297
|
+
#
|
293
298
|
def self.configuration
|
294
299
|
@configuration ||= Configuration.new
|
295
300
|
end
|
@@ -301,5 +306,4 @@ module Authenticate
|
|
301
306
|
def self.configure
|
302
307
|
yield configuration
|
303
308
|
end
|
304
|
-
|
305
309
|
end
|
@@ -1,4 +1,27 @@
|
|
1
1
|
module Authenticate
|
2
|
+
#
|
3
|
+
# The authenticate controller methods.
|
4
|
+
#
|
5
|
+
# Typically, you include this concern into your ApplicationController. A basic implementation might look like this:
|
6
|
+
#
|
7
|
+
# class ApplicationController < ActionController::Base
|
8
|
+
# include Authenticate::Controller
|
9
|
+
# before_action :require_authentication
|
10
|
+
# protect_from_forgery with: :exception
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Methods, generally called from authenticate's app controllers:
|
14
|
+
# * authenticate(params) - validate a user's identity
|
15
|
+
# * login(user, &block) - complete login after validating a user's identity, creating an Authenticate session
|
16
|
+
# * logout - log a user out, invalidating their Authenticate session.
|
17
|
+
#
|
18
|
+
# Action/Filter:
|
19
|
+
# * require_authentication - restrict access to authenticated users, often from ApplicationController
|
20
|
+
#
|
21
|
+
# Helpers, used anywhere:
|
22
|
+
# * current_user - get the current user from the current Authenticate session.
|
23
|
+
# * authenticated? - has the user been logged in?
|
24
|
+
#
|
2
25
|
module Controller
|
3
26
|
extend ActiveSupport::Concern
|
4
27
|
include Debug
|
@@ -8,7 +31,6 @@ module Authenticate
|
|
8
31
|
attr_writer :authenticate_session
|
9
32
|
end
|
10
33
|
|
11
|
-
|
12
34
|
# Validate a user's identity with (typically) email/ID & password, and return the User if valid, or nil.
|
13
35
|
# After calling this, call login(user) to complete the process.
|
14
36
|
def authenticate(params)
|
@@ -16,14 +38,12 @@ module Authenticate
|
|
16
38
|
Authenticate.configuration.user_model_class.authenticate(credentials)
|
17
39
|
end
|
18
40
|
|
19
|
-
|
20
41
|
# Complete the user's sign in process: after calling authenticate, or after user creates account.
|
21
42
|
# Runs all valid callbacks and sends the user a session token.
|
22
43
|
def login(user, &block)
|
23
44
|
authenticate_session.login user, &block
|
24
45
|
end
|
25
46
|
|
26
|
-
|
27
47
|
# Log the user out. Typically used in session controller.
|
28
48
|
#
|
29
49
|
# class SessionsController < ActionController::Base
|
@@ -37,7 +57,6 @@ module Authenticate
|
|
37
57
|
authenticate_session.deauthenticate
|
38
58
|
end
|
39
59
|
|
40
|
-
|
41
60
|
# Use this filter as a before_action to restrict controller actions to authenticated users.
|
42
61
|
# Consider using in application_controller to restrict access to all controllers.
|
43
62
|
#
|
@@ -53,18 +72,14 @@ module Authenticate
|
|
53
72
|
#
|
54
73
|
def require_authentication
|
55
74
|
debug 'Controller::require_authentication'
|
56
|
-
unless authenticated?
|
57
|
-
unauthorized
|
58
|
-
end
|
59
|
-
|
75
|
+
unauthorized unless authenticated?
|
60
76
|
message = catch(:failure) do
|
61
77
|
current_user = authenticate_session.current_user
|
62
|
-
Authenticate.lifecycle.run_callbacks(:after_set_user, current_user, authenticate_session,
|
78
|
+
Authenticate.lifecycle.run_callbacks(:after_set_user, current_user, authenticate_session, event: :set_user)
|
63
79
|
end
|
64
80
|
unauthorized(message) if message
|
65
81
|
end
|
66
82
|
|
67
|
-
|
68
83
|
# Has the user been logged in? Exposed as a helper, can be called from views.
|
69
84
|
#
|
70
85
|
# <% if authenticated? %>
|
@@ -77,7 +92,6 @@ module Authenticate
|
|
77
92
|
authenticate_session.authenticated?
|
78
93
|
end
|
79
94
|
|
80
|
-
|
81
95
|
# Get the current user from the current Authenticate session.
|
82
96
|
# Exposed as a helper , can be called from controllers, views, and other helpers.
|
83
97
|
#
|
@@ -103,9 +117,7 @@ module Authenticate
|
|
103
117
|
authenticate_session.deauthenticate
|
104
118
|
respond_to do |format|
|
105
119
|
format.any(:js, :json, :xml) { head :unauthorized }
|
106
|
-
format.any {
|
107
|
-
redirect_unauthorized(msg)
|
108
|
-
}
|
120
|
+
format.any { redirect_unauthorized(msg) }
|
109
121
|
end
|
110
122
|
end
|
111
123
|
|
@@ -113,7 +125,7 @@ module Authenticate
|
|
113
125
|
store_location
|
114
126
|
|
115
127
|
if flash_message
|
116
|
-
flash[:notice] = flash_message
|
128
|
+
flash[:notice] = flash_message # TODO: use locales
|
117
129
|
end
|
118
130
|
|
119
131
|
if authenticated?
|
@@ -123,13 +135,11 @@ module Authenticate
|
|
123
135
|
end
|
124
136
|
end
|
125
137
|
|
126
|
-
|
127
138
|
def redirect_back_or(default)
|
128
139
|
redirect_to(stored_location || default)
|
129
140
|
clear_stored_location
|
130
141
|
end
|
131
142
|
|
132
|
-
|
133
143
|
# Used as the redirect location when {#unauthorized} is called and there is a
|
134
144
|
# currently signed in user.
|
135
145
|
#
|
@@ -152,11 +162,11 @@ module Authenticate
|
|
152
162
|
def store_location
|
153
163
|
if request.get?
|
154
164
|
value = {
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
165
|
+
expires: nil,
|
166
|
+
httponly: true,
|
167
|
+
path: nil,
|
168
|
+
secure: Authenticate.configuration.secure_cookie,
|
169
|
+
value: request.original_fullpath
|
160
170
|
}
|
161
171
|
cookies[:authenticate_return_to] = value
|
162
172
|
end
|
@@ -173,6 +183,5 @@ module Authenticate
|
|
173
183
|
def authenticate_session
|
174
184
|
@authenticate_session ||= Authenticate::Session.new(request, cookies)
|
175
185
|
end
|
176
|
-
|
177
186
|
end
|
178
187
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Authenticate
|
2
2
|
module Crypto
|
3
|
-
|
3
|
+
#
|
4
4
|
# All crypto providers must implement encrypt(secret) and match?(secret, encrypted)
|
5
5
|
module BCrypt
|
6
6
|
require 'bcrypt'
|
@@ -20,11 +20,11 @@ module Authenticate
|
|
20
20
|
|
21
21
|
def cost=(val)
|
22
22
|
if val < ::BCrypt::Engine::MIN_COST
|
23
|
-
|
23
|
+
msg = "bcrypt cost cannot be set below the engine's min cost (#{::BCrypt::Engine::MIN_COST})"
|
24
|
+
raise ArgumentError.new(msg), msg
|
24
25
|
end
|
25
26
|
@cost = val
|
26
27
|
end
|
27
|
-
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
data/lib/authenticate/debug.rb
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
module Authenticate
|
2
|
+
#
|
3
|
+
# Simple debug output for gem.
|
4
|
+
#
|
2
5
|
module Debug
|
3
6
|
extend ActiveSupport::Concern
|
4
7
|
|
5
|
-
|
6
8
|
def debug(msg)
|
7
|
-
if defined?(Rails) && defined?(Rails.logger)
|
8
|
-
Rails.logger.info msg.to_s
|
9
|
-
|
10
|
-
puts msg.to_s
|
9
|
+
if defined?(Rails) && defined?(Rails.logger) && Authenticate.configuration.debug
|
10
|
+
Rails.logger.info msg.to_s
|
11
|
+
elsif Authenticate.configuration.debug
|
12
|
+
puts msg.to_s
|
11
13
|
end
|
12
14
|
end
|
13
|
-
|
14
|
-
|
15
15
|
end
|
16
16
|
end
|
data/lib/authenticate/engine.rb
CHANGED
@@ -2,8 +2,11 @@ require 'authenticate'
|
|
2
2
|
require 'rails'
|
3
3
|
|
4
4
|
module Authenticate
|
5
|
+
#
|
6
|
+
# Authenticate Rails engine.
|
7
|
+
# Filter password, token, from spewing out.
|
8
|
+
#
|
5
9
|
class Engine < ::Rails::Engine
|
6
|
-
|
7
10
|
initializer 'authenticate.filter' do |app|
|
8
11
|
app.config.filter_parameters += [:password, :token]
|
9
12
|
end
|
@@ -12,6 +15,5 @@ module Authenticate
|
|
12
15
|
g.test_framework :rspec
|
13
16
|
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
14
17
|
end
|
15
|
-
|
16
18
|
end
|
17
19
|
end
|
@@ -1,5 +1,6 @@
|
|
1
|
+
# Authenticate Lifecycle methods within
|
1
2
|
module Authenticate
|
2
|
-
|
3
|
+
#
|
3
4
|
# Lifecycle stores and runs callbacks for authorization events.
|
4
5
|
#
|
5
6
|
# Heavily borrowed from warden (https://github.com/hassox/warden).
|
@@ -37,22 +38,22 @@ module Authenticate
|
|
37
38
|
#
|
38
39
|
class Lifecycle
|
39
40
|
include Debug
|
40
|
-
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
@conditions = [:only, :except, :event].freeze
|
44
|
+
end
|
41
45
|
|
42
46
|
# This callback is triggered after the first time a user is set during per-hit authorization, or during login.
|
43
47
|
def after_set_user(options = {}, method = :push, &block)
|
44
48
|
add_callback(after_set_user_callbacks, options, method, &block)
|
45
49
|
end
|
46
50
|
|
47
|
-
|
48
|
-
|
49
51
|
# A callback to run after the user successfully authenticates, during the login process.
|
50
52
|
# Mechanically identical to [#after_set_user].
|
51
53
|
def after_authentication(options = {}, method = :push, &block)
|
52
54
|
add_callback(after_authentication_callbacks, options, method, &block)
|
53
55
|
end
|
54
56
|
|
55
|
-
|
56
57
|
# Run callbacks of the given kind.
|
57
58
|
#
|
58
59
|
# * kind - :authenticate or :after_set_user
|
@@ -64,27 +65,16 @@ module Authenticate
|
|
64
65
|
def run_callbacks(kind, *args) # args - |user, session, opts|
|
65
66
|
# Last callback arg MUST be a Hash
|
66
67
|
options = args.last
|
67
|
-
|
68
|
-
|
69
|
-
# each callback has 'conditions' stored with it
|
70
|
-
send("#{kind}_callbacks").each do |callback, conditions|
|
71
|
-
conditions = conditions.dup # make a copy, we mutate it
|
72
|
-
debug "Lifecycle.running callback -- #{conditions.inspect}"
|
73
|
-
conditions.delete_if {|key, _val| !@@conditions.include? key}
|
74
|
-
# debug "conditions after filter:#{conditions.inspect}"
|
68
|
+
send("#{kind}_callbacks").each do |callback, conditions| # each callback has 'conditions' stored with it
|
69
|
+
conditions = conditions.dup.delete_if { |key, _val| !@conditions.include? key }
|
75
70
|
invalid = conditions.find do |key, value|
|
76
|
-
# debug "!!!!!!! conditions key:#{key} value:#{value} options[key]:#{options[key].inspect}"
|
77
|
-
# debug("!value.include?(options[key]):#{!value.include?(options[key])}") if value.is_a?(Array)
|
78
71
|
value.is_a?(Array) ? !value.include?(options[key]) : (value != options[key])
|
79
72
|
end
|
80
|
-
debug "Lifecycle.callback invalid? #{invalid.inspect}"
|
81
73
|
callback.call(*args) unless invalid
|
82
74
|
end
|
83
|
-
debug "FINISHED Lifecycle.run_callbacks #{kind}"
|
84
75
|
nil
|
85
76
|
end
|
86
77
|
|
87
|
-
|
88
78
|
def prepend_after_authentication(options = {}, &block)
|
89
79
|
after_authentication(options, :unshift, &block)
|
90
80
|
end
|
@@ -97,7 +87,6 @@ module Authenticate
|
|
97
87
|
callbacks.send(method, [block, options])
|
98
88
|
end
|
99
89
|
|
100
|
-
|
101
90
|
# set event: to run callback on based on options
|
102
91
|
def process_opts(options)
|
103
92
|
if options.key?(:only)
|
@@ -108,7 +97,6 @@ module Authenticate
|
|
108
97
|
options
|
109
98
|
end
|
110
99
|
|
111
|
-
|
112
100
|
def after_set_user_callbacks
|
113
101
|
@after_set_user_callbacks ||= []
|
114
102
|
end
|
@@ -118,7 +106,9 @@ module Authenticate
|
|
118
106
|
end
|
119
107
|
end
|
120
108
|
|
121
|
-
|
109
|
+
# Invoke lifecycle methods. Example:
|
110
|
+
# Authenticate.lifecycle.run_callbacks(:after_set_user, current_user, authenticate_session, { event: :set_user })
|
111
|
+
#
|
122
112
|
def self.lifecycle
|
123
113
|
@lifecycle ||= Lifecycle.new
|
124
114
|
end
|
@@ -126,4 +116,4 @@ module Authenticate
|
|
126
116
|
def self.lifecycle=(lifecycle)
|
127
117
|
@lifecycle = lifecycle
|
128
118
|
end
|
129
|
-
end
|
119
|
+
end
|
@@ -1,14 +1,17 @@
|
|
1
1
|
module Authenticate
|
2
|
-
|
2
|
+
#
|
3
3
|
# Indicate login attempt was successful. Allows caller to supply a block to login() predicated on success?
|
4
|
+
#
|
4
5
|
class Success
|
5
6
|
def success?
|
6
7
|
true
|
7
8
|
end
|
8
9
|
end
|
9
10
|
|
11
|
+
#
|
10
12
|
# Indicate login attempt was a failure, with a message.
|
11
13
|
# Allows caller to supply a block to login() predicated on success?
|
14
|
+
#
|
12
15
|
class Failure
|
13
16
|
# The reason the sign in failed.
|
14
17
|
attr_reader :message
|
@@ -22,6 +25,4 @@ module Authenticate
|
|
22
25
|
false
|
23
26
|
end
|
24
27
|
end
|
25
|
-
|
26
28
|
end
|
27
|
-
|
@@ -2,8 +2,7 @@ require 'authenticate/callbacks/brute_force'
|
|
2
2
|
|
3
3
|
module Authenticate
|
4
4
|
module Model
|
5
|
-
|
6
|
-
|
5
|
+
#
|
7
6
|
# Protect from brute force attacks. Lock accounts that have too many failed consecutive logins.
|
8
7
|
# Todo: email user to allow unlocking via a token.
|
9
8
|
#
|
@@ -37,7 +36,6 @@ module Authenticate
|
|
37
36
|
[:failed_logins_count, :lock_expires_at]
|
38
37
|
end
|
39
38
|
|
40
|
-
|
41
39
|
def register_failed_login!
|
42
40
|
self.failed_logins_count ||= 0
|
43
41
|
self.failed_logins_count += 1
|
@@ -45,11 +43,11 @@ module Authenticate
|
|
45
43
|
end
|
46
44
|
|
47
45
|
def lock!
|
48
|
-
|
46
|
+
update_attribute(:lock_expires_at, Time.now.utc + lockout_period)
|
49
47
|
end
|
50
48
|
|
51
49
|
def unlock!
|
52
|
-
|
50
|
+
update_attributes(failed_logins_count: 0, lock_expires_at: nil)
|
53
51
|
end
|
54
52
|
|
55
53
|
def locked?
|
@@ -57,7 +55,7 @@ module Authenticate
|
|
57
55
|
end
|
58
56
|
|
59
57
|
def unlocked?
|
60
|
-
|
58
|
+
lock_expires_at.nil?
|
61
59
|
end
|
62
60
|
|
63
61
|
private
|
@@ -1,9 +1,8 @@
|
|
1
1
|
require 'authenticate/crypto/bcrypt'
|
2
2
|
|
3
|
-
|
4
3
|
module Authenticate
|
5
4
|
module Model
|
6
|
-
|
5
|
+
#
|
7
6
|
# Encrypts and stores a password in the database to validate the authenticity of a user while logging in.
|
8
7
|
#
|
9
8
|
# Authenticate can plug in any crypto provider, but currently only features BCrypt.
|
@@ -37,28 +36,23 @@ module Authenticate
|
|
37
36
|
attr_accessor :password_changing
|
38
37
|
validates :password,
|
39
38
|
presence: true,
|
40
|
-
length:{ in: password_length },
|
39
|
+
length: { in: password_length },
|
41
40
|
unless: :skip_password_validation?
|
42
41
|
end
|
43
42
|
|
44
|
-
|
45
|
-
|
46
43
|
def password_match?(password)
|
47
|
-
match?(password,
|
44
|
+
match?(password, encrypted_password)
|
48
45
|
end
|
49
46
|
|
50
47
|
def password=(new_password)
|
51
48
|
@password = new_password
|
52
|
-
|
53
|
-
if new_password.present?
|
54
|
-
self.encrypted_password = encrypt(new_password)
|
55
|
-
end
|
49
|
+
self.encrypted_password = encrypt(new_password) if new_password.present?
|
56
50
|
end
|
57
51
|
|
58
52
|
private
|
59
53
|
|
54
|
+
# Class methods for database password management.
|
60
55
|
module ClassMethods
|
61
|
-
|
62
56
|
# We only have one crypto provider at the moment, but look up the provider in the config.
|
63
57
|
def crypto_provider
|
64
58
|
Authenticate.configuration.crypto_provider || Authenticate::Crypto::BCrypt
|
@@ -69,13 +63,10 @@ module Authenticate
|
|
69
63
|
end
|
70
64
|
end
|
71
65
|
|
72
|
-
|
73
66
|
# If we already have an encrypted password and it's not changing, skip the validation.
|
74
67
|
def skip_password_validation?
|
75
68
|
encrypted_password.present? && !password_changing
|
76
69
|
end
|
77
|
-
|
78
70
|
end
|
79
71
|
end
|
80
72
|
end
|
81
|
-
|