linked_rails-auth 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +28 -0
- data/Rakefile +34 -0
- data/app/actions/linked_rails/auth/access_token_action_list.rb +16 -0
- data/app/actions/linked_rails/auth/confirmation_action_list.rb +17 -0
- data/app/actions/linked_rails/auth/otp_attempt_action_list.rb +13 -0
- data/app/actions/linked_rails/auth/otp_secret_action_list.rb +31 -0
- data/app/actions/linked_rails/auth/password_action_list.rb +25 -0
- data/app/actions/linked_rails/auth/registration_action_list.rb +15 -0
- data/app/actions/linked_rails/auth/session_action_list.rb +22 -0
- data/app/actions/linked_rails/auth/unlock_action_list.rb +17 -0
- data/app/controllers/linked_rails/auth/access_tokens_controller.rb +131 -0
- data/app/controllers/linked_rails/auth/confirmations_controller.rb +87 -0
- data/app/controllers/linked_rails/auth/otp_attempts_controller.rb +21 -0
- data/app/controllers/linked_rails/auth/otp_secrets_controller.rb +40 -0
- data/app/controllers/linked_rails/auth/passwords_controller.rb +63 -0
- data/app/controllers/linked_rails/auth/registrations_controller.rb +33 -0
- data/app/controllers/linked_rails/auth/sessions_controller.rb +55 -0
- data/app/controllers/linked_rails/auth/unlocks_controller.rb +44 -0
- data/app/forms/linked_rails/auth/access_token_form.rb +20 -0
- data/app/forms/linked_rails/auth/confirmation_form.rb +9 -0
- data/app/forms/linked_rails/auth/otp_attempt_form.rb +9 -0
- data/app/forms/linked_rails/auth/otp_secret_form.rb +12 -0
- data/app/forms/linked_rails/auth/password_form.rb +21 -0
- data/app/forms/linked_rails/auth/registration_form.rb +21 -0
- data/app/forms/linked_rails/auth/session_form.rb +13 -0
- data/app/forms/linked_rails/auth/unlock_form.rb +9 -0
- data/app/helpers/linked_rails/auth/otp_helper.rb +30 -0
- data/app/models/linked_rails/auth/access_token.rb +40 -0
- data/app/models/linked_rails/auth/confirmation.rb +70 -0
- data/app/models/linked_rails/auth/guest_user.rb +29 -0
- data/app/models/linked_rails/auth/otp_attempt.rb +41 -0
- data/app/models/linked_rails/auth/otp_base.rb +57 -0
- data/app/models/linked_rails/auth/otp_secret.rb +91 -0
- data/app/models/linked_rails/auth/password.rb +47 -0
- data/app/models/linked_rails/auth/registration.rb +39 -0
- data/app/models/linked_rails/auth/session.rb +46 -0
- data/app/models/linked_rails/auth/unlock.rb +59 -0
- data/app/policies/linked_rails/auth/access_token_policy.rb +17 -0
- data/app/policies/linked_rails/auth/confirmation_policy.rb +17 -0
- data/app/policies/linked_rails/auth/otp_attempt_policy.rb +17 -0
- data/app/policies/linked_rails/auth/otp_secret_policy.rb +37 -0
- data/app/policies/linked_rails/auth/password_policy.rb +23 -0
- data/app/policies/linked_rails/auth/registration_policy.rb +13 -0
- data/app/policies/linked_rails/auth/session_policy.rb +17 -0
- data/app/policies/linked_rails/auth/unlock_policy.rb +21 -0
- data/app/serializers/linked_rails/auth/access_token_serializer.rb +11 -0
- data/app/serializers/linked_rails/auth/confirmation_serializer.rb +10 -0
- data/app/serializers/linked_rails/auth/otp_attempt_serializer.rb +12 -0
- data/app/serializers/linked_rails/auth/otp_secret_serializer.rb +14 -0
- data/app/serializers/linked_rails/auth/password_serializer.rb +18 -0
- data/app/serializers/linked_rails/auth/registration_serializer.rb +12 -0
- data/app/serializers/linked_rails/auth/session_serializer.rb +10 -0
- data/app/serializers/linked_rails/auth/unlock_serializer.rb +9 -0
- data/config/routes.rb +4 -0
- data/lib/generators/linked_rails/auth/install_generator.rb +155 -0
- data/lib/generators/linked_rails/auth/templates/README +2 -0
- data/lib/generators/linked_rails/auth/templates/doorkeeper_jwt_initializer.rb +52 -0
- data/lib/generators/linked_rails/auth/templates/locales.yml +39 -0
- data/lib/generators/linked_rails/auth/templates/migration.rb.erb +20 -0
- data/lib/linked_rails/auth/auth_helper.rb +101 -0
- data/lib/linked_rails/auth/controller/error_handling.rb +17 -0
- data/lib/linked_rails/auth/controller.rb +16 -0
- data/lib/linked_rails/auth/engine.rb +8 -0
- data/lib/linked_rails/auth/errors/account_locked.rb +10 -0
- data/lib/linked_rails/auth/errors/expired.rb +10 -0
- data/lib/linked_rails/auth/errors/unauthorized.rb +10 -0
- data/lib/linked_rails/auth/errors/unknown_email.rb +14 -0
- data/lib/linked_rails/auth/errors/wrong_password.rb +14 -0
- data/lib/linked_rails/auth/errors.rb +7 -0
- data/lib/linked_rails/auth/routes.rb +86 -0
- data/lib/linked_rails/auth/version.rb +7 -0
- data/lib/linked_rails/auth.rb +46 -0
- data/lib/tasks/linked_rails/auth_tasks.rake +6 -0
- metadata +257 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class Confirmation < LinkedRails::Resource
|
6
|
+
enhance LinkedRails::Enhancements::Actionable
|
7
|
+
enhance LinkedRails::Enhancements::Creatable
|
8
|
+
enhance LinkedRails::Enhancements::Updatable
|
9
|
+
enhance LinkedRails::Enhancements::Singularable
|
10
|
+
attr_accessor :confirmation_token, :email, :user, :password_token
|
11
|
+
alias root_relative_iri root_relative_singular_iri
|
12
|
+
|
13
|
+
def anonymous_iri?
|
14
|
+
confirmation_token.blank?
|
15
|
+
end
|
16
|
+
|
17
|
+
def confirm!
|
18
|
+
owner!.confirm
|
19
|
+
end
|
20
|
+
|
21
|
+
delegate :confirmed?, to: :owner!
|
22
|
+
|
23
|
+
def singular_iri_opts
|
24
|
+
{confirmation_token: confirmation_token}
|
25
|
+
end
|
26
|
+
|
27
|
+
def redirect_url
|
28
|
+
LinkedRails.iri
|
29
|
+
end
|
30
|
+
|
31
|
+
def owner!
|
32
|
+
owner || raise(ActiveRecord::RecordNotFound)
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def action_list
|
37
|
+
LinkedRails.confirmation_action_list_class
|
38
|
+
end
|
39
|
+
|
40
|
+
def form_class
|
41
|
+
LinkedRails.confirmation_form_class
|
42
|
+
end
|
43
|
+
|
44
|
+
def iri_namespace
|
45
|
+
Vocab.ontola
|
46
|
+
end
|
47
|
+
|
48
|
+
def singular_iri_template
|
49
|
+
@singular_iri_template ||= URITemplate.new("/#{singular_route_key}{?confirmation_token}")
|
50
|
+
end
|
51
|
+
|
52
|
+
def requested_singular_resource(params, _user_context)
|
53
|
+
return new unless params.key?(:confirmation_token)
|
54
|
+
|
55
|
+
user_by_token = LinkedRails.user_class.find_by(confirmation_token: params[:confirmation_token])
|
56
|
+
return if user_by_token.blank?
|
57
|
+
|
58
|
+
new(
|
59
|
+
confirmation_token: params[:confirmation_token],
|
60
|
+
user: user_by_token
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def singular_route_key
|
65
|
+
'u/confirmation'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class GuestUser
|
6
|
+
include ActiveModel::Model
|
7
|
+
include LinkedRails::Model
|
8
|
+
attr_writer :id
|
9
|
+
|
10
|
+
def email; end
|
11
|
+
|
12
|
+
def guest?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
@id ||= SecureRandom.hex
|
18
|
+
end
|
19
|
+
|
20
|
+
def iri_opts
|
21
|
+
{id: id}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.iri
|
25
|
+
Vocab.ontola[:GuestUser]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class OtpAttempt < OtpBase
|
6
|
+
def raise_on_persisting(_opts = {})
|
7
|
+
raise "#{self.class.name} should not be persisted"
|
8
|
+
end
|
9
|
+
ActiveRecord::Persistence.instance_methods.each do |method|
|
10
|
+
alias_method method, :raise_on_persisting unless method.to_s.include?('?')
|
11
|
+
end
|
12
|
+
|
13
|
+
alias root_relative_iri root_relative_singular_iri
|
14
|
+
|
15
|
+
def save
|
16
|
+
validate_otp_attempt
|
17
|
+
|
18
|
+
errors.empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def form_class
|
23
|
+
LinkedRails.otp_attempt_form_class
|
24
|
+
end
|
25
|
+
|
26
|
+
def singular_route_key
|
27
|
+
'u/otp_attempt'
|
28
|
+
end
|
29
|
+
|
30
|
+
def requested_singular_resource(params, user_context)
|
31
|
+
owner = owner_for_otp(params, user_context)
|
32
|
+
return if owner.blank?
|
33
|
+
|
34
|
+
attempt = LinkedRails.otp_attempt_class.find_by(owner: owner) || LinkedRails.otp_attempt_class.new
|
35
|
+
attempt.encoded_session = params[:session]
|
36
|
+
attempt
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class OtpBase < ApplicationRecord
|
6
|
+
self.table_name = 'otp_secrets'
|
7
|
+
self.abstract_class = true
|
8
|
+
|
9
|
+
enhance LinkedRails::Enhancements::Actionable
|
10
|
+
enhance LinkedRails::Enhancements::Creatable
|
11
|
+
enhance LinkedRails::Enhancements::Singularable
|
12
|
+
|
13
|
+
extend OtpHelper
|
14
|
+
include OtpHelper
|
15
|
+
|
16
|
+
has_one_time_password
|
17
|
+
belongs_to :owner, class_name: LinkedRails.otp_owner_class.to_s
|
18
|
+
validates :owner, presence: true
|
19
|
+
|
20
|
+
attr_accessor :encoded_session, :otp_attempt
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def decoded_session
|
25
|
+
@decoded_session ||= session_from_param(encoded_session)
|
26
|
+
end
|
27
|
+
|
28
|
+
def singular_iri_opts
|
29
|
+
{session: encoded_session}
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_otp_attempt
|
33
|
+
return if persisted? && authenticate_otp(otp_attempt, drift: LinkedRails::Auth.otp_drift)
|
34
|
+
|
35
|
+
errors.add(:otp_attempt, I18n.t('messages.otp_secrets.invalid'))
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
def iri_template
|
40
|
+
@iri_template ||= URITemplate.new("/#{route_key}{/id}{?session}{#fragment}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def singular_iri_template
|
44
|
+
@singular_iri_template ||= URITemplate.new("{/parent_iri*}/#{singular_route_key}{?session}{#fragment}")
|
45
|
+
end
|
46
|
+
|
47
|
+
def owner_for_otp(params, user_context)
|
48
|
+
if params.key?(:session)
|
49
|
+
owner_from_session(params[:session])
|
50
|
+
else
|
51
|
+
user_context unless user_context.guest?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rqrcode'
|
4
|
+
|
5
|
+
module LinkedRails
|
6
|
+
module Auth
|
7
|
+
class OtpSecret < OtpBase
|
8
|
+
enhance LinkedRails::Enhancements::Destroyable
|
9
|
+
|
10
|
+
validate :validate_otp_attempt, on: %i[update]
|
11
|
+
|
12
|
+
def image
|
13
|
+
return if active? || !persisted?
|
14
|
+
|
15
|
+
@image ||=
|
16
|
+
LinkedRails::MediaObject.new(
|
17
|
+
content_url: data_url,
|
18
|
+
content_type: 'image/png',
|
19
|
+
iri: LinkedRails.iri(path: root_relative_iri, fragment: 'image')
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def iri_opts
|
24
|
+
{id: id}
|
25
|
+
end
|
26
|
+
|
27
|
+
def redirect_url
|
28
|
+
decoded_session['redirect_uri'] || LinkedRails.iri.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def data_url
|
34
|
+
[
|
35
|
+
'data:image/svg+xml;base64,',
|
36
|
+
RQRCode::QRCode.new(provisioning_uri(owner.email, issuer: issuer)).as_svg(module_size: 4).to_s
|
37
|
+
].pack('A*m').delete("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def issuer
|
41
|
+
return issuer_name if Rails.env.production?
|
42
|
+
|
43
|
+
"#{issuer_name} #{Rails.env}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def issuer_name
|
47
|
+
Rails.application.railtie_name.chomp('_application').humanize
|
48
|
+
end
|
49
|
+
|
50
|
+
class << self
|
51
|
+
def activated?(owner_id)
|
52
|
+
LinkedRails.otp_secret_class.exists?(
|
53
|
+
owner_id: owner_id,
|
54
|
+
active: true
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def form_class
|
59
|
+
LinkedRails.otp_secret_form_class
|
60
|
+
end
|
61
|
+
|
62
|
+
def preview_includes
|
63
|
+
%i[image]
|
64
|
+
end
|
65
|
+
|
66
|
+
def requested_singular_resource(params, user_context)
|
67
|
+
owner = owner_for_otp(params, user_context)
|
68
|
+
return if owner.blank?
|
69
|
+
|
70
|
+
secret = LinkedRails.otp_secret_class.find_or_create_by!(owner: owner)
|
71
|
+
secret.encoded_session = params[:session]
|
72
|
+
secret
|
73
|
+
rescue ActiveRecord::RecordNotUnique
|
74
|
+
requested_singular_resource(params, user_context)
|
75
|
+
end
|
76
|
+
|
77
|
+
def requested_single_resource(params, _user_context)
|
78
|
+
LinkedRails.otp_secret_class.find_by(id: params[:id])
|
79
|
+
end
|
80
|
+
|
81
|
+
def route_key
|
82
|
+
'u/otp_secrets'
|
83
|
+
end
|
84
|
+
|
85
|
+
def singular_route_key
|
86
|
+
'u/otp_secret'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class Password < LinkedRails::Resource
|
6
|
+
enhance LinkedRails::Enhancements::Actionable
|
7
|
+
enhance LinkedRails::Enhancements::Creatable
|
8
|
+
enhance LinkedRails::Enhancements::Updatable
|
9
|
+
enhance LinkedRails::Enhancements::Singularable
|
10
|
+
attr_accessor :email, :password, :password_confirmation, :user, :reset_password_token
|
11
|
+
alias root_relative_iri root_relative_singular_iri
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def action_list
|
15
|
+
LinkedRails.password_action_list_class
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt_token(token)
|
19
|
+
Devise.token_generator.digest(self, :reset_password_token, token)
|
20
|
+
end
|
21
|
+
|
22
|
+
def form_class
|
23
|
+
LinkedRails.password_form_class
|
24
|
+
end
|
25
|
+
|
26
|
+
def iri_namespace
|
27
|
+
Vocab.ontola
|
28
|
+
end
|
29
|
+
|
30
|
+
def requested_singular_resource(params, _user_context)
|
31
|
+
reset_password_token = decrypt_token(params[:reset_password_token])
|
32
|
+
user_by_token ||= LinkedRails.user_class.find_by(reset_password_token: reset_password_token)
|
33
|
+
return new(reset_password_token: params[:reset_password_token]) if user_by_token.blank?
|
34
|
+
|
35
|
+
new(
|
36
|
+
reset_password_token: params[:reset_password_token],
|
37
|
+
user: user_by_token
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def singular_route_key
|
42
|
+
'u/password'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class Registration < LinkedRails.user_class
|
6
|
+
enhance LinkedRails::Enhancements::Actionable
|
7
|
+
enhance LinkedRails::Enhancements::Creatable
|
8
|
+
enhance LinkedRails::Enhancements::Singularable
|
9
|
+
|
10
|
+
attr_accessor :redirect_url
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def action_list
|
14
|
+
LinkedRails.registration_action_list_class
|
15
|
+
end
|
16
|
+
|
17
|
+
def form_class
|
18
|
+
LinkedRails.registration_form_class
|
19
|
+
end
|
20
|
+
|
21
|
+
def iri_namespace
|
22
|
+
Vocab.ontola
|
23
|
+
end
|
24
|
+
|
25
|
+
def iri_template
|
26
|
+
LinkedRails.user_class.iri_template
|
27
|
+
end
|
28
|
+
|
29
|
+
def requested_singular_resource(_params, user_context)
|
30
|
+
build_new(user_context: user_context)
|
31
|
+
end
|
32
|
+
|
33
|
+
def singular_route_key
|
34
|
+
'u/registration'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class Session < LinkedRails::Resource
|
6
|
+
enhance LinkedRails::Enhancements::Actionable
|
7
|
+
enhance LinkedRails::Enhancements::Creatable
|
8
|
+
enhance LinkedRails::Enhancements::Destroyable
|
9
|
+
enhance LinkedRails::Enhancements::Singularable
|
10
|
+
alias root_relative_iri root_relative_singular_iri
|
11
|
+
|
12
|
+
attr_accessor :email, :redirect_url
|
13
|
+
|
14
|
+
def singular_iri_opts
|
15
|
+
{redirect_url: redirect_url}
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def action_list
|
20
|
+
LinkedRails.session_action_list_class
|
21
|
+
end
|
22
|
+
|
23
|
+
def form_class
|
24
|
+
LinkedRails.session_form_class
|
25
|
+
end
|
26
|
+
|
27
|
+
def iri_namespace
|
28
|
+
Vocab.ontola
|
29
|
+
end
|
30
|
+
|
31
|
+
def singular_iri_template
|
32
|
+
@singular_iri_template ||= URITemplate.new("/#{singular_route_key}{?redirect_url}")
|
33
|
+
end
|
34
|
+
alias iri_template singular_iri_template
|
35
|
+
|
36
|
+
def requested_singular_resource(params, _user_context)
|
37
|
+
new(redirect_url: params[:redirect_url])
|
38
|
+
end
|
39
|
+
|
40
|
+
def singular_route_key
|
41
|
+
'u/session'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LinkedRails
|
4
|
+
module Auth
|
5
|
+
class Unlock < LinkedRails::Resource
|
6
|
+
enhance LinkedRails::Enhancements::Actionable
|
7
|
+
enhance LinkedRails::Enhancements::Creatable
|
8
|
+
enhance LinkedRails::Enhancements::Updatable, except: %i[Serializer]
|
9
|
+
enhance LinkedRails::Enhancements::Singularable
|
10
|
+
attr_accessor :email, :unlock_token, :user
|
11
|
+
alias root_relative_iri root_relative_singular_iri
|
12
|
+
|
13
|
+
def anonymous_iri?
|
14
|
+
unlock_token.blank?
|
15
|
+
end
|
16
|
+
|
17
|
+
def singular_iri_opts
|
18
|
+
{unlock_token: unlock_token}
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def action_list
|
23
|
+
LinkedRails.unlock_action_list_class
|
24
|
+
end
|
25
|
+
|
26
|
+
def decrypt_token(token)
|
27
|
+
Devise.token_generator.digest(self, :unlock_token, token)
|
28
|
+
end
|
29
|
+
|
30
|
+
def form_class
|
31
|
+
LinkedRails.unlock_form_class
|
32
|
+
end
|
33
|
+
|
34
|
+
def iri_namespace
|
35
|
+
Vocab.ontola
|
36
|
+
end
|
37
|
+
|
38
|
+
def singular_iri_template
|
39
|
+
@singular_iri_template ||= URITemplate.new("/#{singular_route_key}{?unlock_token}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def singular_route_key
|
43
|
+
'u/unlock'
|
44
|
+
end
|
45
|
+
|
46
|
+
def requested_singular_resource(params, _user_context)
|
47
|
+
token = decrypt_token(token)
|
48
|
+
user_by_token ||= LinkedRails.user_class.find_by(unlock_token: token)
|
49
|
+
return new(unlock_token: params[:unlock_token]) if user_by_token.blank?
|
50
|
+
|
51
|
+
new(
|
52
|
+
unlock_token: params[:unlock_token],
|
53
|
+
user: user_by_token
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|