secretkeeper 0.1.0 → 0.1.1
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 +5 -5
- data/README.md +35 -4
- data/Rakefile +6 -9
- data/app/controllers/secretkeeper/auth_controller.rb +30 -26
- data/lib/generators/secretkeeper/install_generator.rb +28 -20
- data/lib/generators/secretkeeper/templates/migration.rb +1 -1
- data/lib/secretkeeper.rb +6 -4
- data/lib/secretkeeper/access_token.rb +27 -11
- data/lib/secretkeeper/configuration.rb +27 -0
- data/lib/secretkeeper/engine.rb +4 -2
- data/lib/secretkeeper/rails/helpers.rb +26 -8
- data/lib/secretkeeper/rails/router.rb +3 -1
- data/lib/secretkeeper/version.rb +3 -1
- metadata +43 -17
- data/lib/secretkeeper/config.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4aaccaa792079587f03048814194318bda9674d49349d0b718198ae056695820
|
4
|
+
data.tar.gz: bbcadf0fcb47cd9a74a9cf8d2ea09877b2325df7ab6490fc1c01927f0db82cee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ed5e626b3ed28f2965672f4cde83e154cb3d162caff25d08e258269ffdb730cc06ffd756b0c249f7b89a91ff930f4389e4cf65cdfb3a0c689d0d776f39eed9f
|
7
|
+
data.tar.gz: 61799d52dc349a0639d6677d88411a87a5bbbf343e22eeee747417933e2122a3128d4fa2f44a080ab5c1508c6315f2b0a3895b94ac1f34a9a684086ae0bf1495
|
data/README.md
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
# Secretkeeper
|
2
|
-
|
3
|
-
|
4
|
-
## Usage
|
5
|
-
How to use my plugin.
|
2
|
+
Token based authentication.
|
6
3
|
|
7
4
|
## Installation
|
5
|
+
|
8
6
|
Add this line to your application's Gemfile:
|
9
7
|
|
10
8
|
```ruby
|
@@ -26,5 +24,38 @@ Run the installation generator with:
|
|
26
24
|
rails generate secretkeeper:install
|
27
25
|
```
|
28
26
|
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Authorizing requests
|
30
|
+
|
31
|
+
``` ruby
|
32
|
+
class Api::BaseController < ActionController::API
|
33
|
+
before_action :secretkeeper_authorize!
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
### Authenticating
|
38
|
+
|
39
|
+
`config/initializers/secretkeeper.rb`:
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
Secretkeeper.reflect do |on|
|
43
|
+
on.resource_owner do |params|
|
44
|
+
User.authenticate!(params[:name], params[:password])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
### Routes
|
50
|
+
|
51
|
+
The installation script will also automatically add the Secretkeeper routes into your app.
|
52
|
+
|
53
|
+
``` ruby
|
54
|
+
Rails.application.routes.draw do
|
55
|
+
secretkeeper
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
29
59
|
## License
|
60
|
+
|
30
61
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
# begin
|
5
7
|
# require 'bundler/setup'
|
@@ -17,11 +19,6 @@ require "rspec/core/rake_task"
|
|
17
19
|
# rdoc.rdoc_files.include('lib/**/*.rb')
|
18
20
|
# end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
22
|
# require 'bundler/gem_tasks'
|
26
23
|
|
27
24
|
# require 'rake/testtask'
|
@@ -32,10 +29,10 @@ require "rspec/core/rake_task"
|
|
32
29
|
# t.verbose = false
|
33
30
|
# end
|
34
31
|
|
35
|
-
desc
|
32
|
+
desc 'Default: run specs.'
|
36
33
|
task default: :spec
|
37
34
|
|
38
|
-
desc
|
35
|
+
desc 'Run all specs'
|
39
36
|
RSpec::Core::RakeTask.new(:spec) do |config|
|
40
37
|
config.verbose = false
|
41
38
|
end
|
@@ -1,41 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Secretkeeper
|
2
4
|
class AuthController < ActionController::Metal
|
3
5
|
include AbstractController::Rendering
|
4
|
-
include ActionController::Rendering
|
5
|
-
include ActionController::Renderers
|
6
6
|
include ActionController::Head
|
7
|
+
include ActionController::Renderers
|
8
|
+
include ActionController::Rendering
|
7
9
|
|
8
10
|
use_renderers :json
|
9
11
|
|
10
12
|
def create_token
|
11
13
|
resource_owner = Secretkeeper.reflection_resource_owner.call(params)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
refresh_token: access_token.refresh_token,
|
18
|
-
created_at: access_token.created_at.to_i
|
19
|
-
}, status: 201
|
20
|
-
else
|
21
|
-
head 401
|
22
|
-
end
|
14
|
+
|
15
|
+
return head 401 if resource_owner.nil?
|
16
|
+
|
17
|
+
@access_token = Secretkeeper::AccessToken.create(owner: resource_owner)
|
18
|
+
render json: success, status: 201
|
23
19
|
end
|
24
20
|
|
25
21
|
def refresh_token
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
22
|
+
return head 403 unless old_token&.refreshable?
|
23
|
+
|
24
|
+
old_token.revoke!
|
25
|
+
@access_token = Secretkeeper::AccessToken.create(owner: old_token.owner)
|
26
|
+
render json: success
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def success
|
32
|
+
{
|
33
|
+
access_token: @access_token.token,
|
34
|
+
expires_in: @access_token.expires_in,
|
35
|
+
refresh_token: @access_token.refresh_token,
|
36
|
+
created_at: @access_token.created_at.to_i
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def old_token
|
41
|
+
@old_token ||=
|
42
|
+
Secretkeeper::AccessToken.find_by(refresh_token: params[:refresh_token])
|
39
43
|
end
|
40
44
|
end
|
41
45
|
end
|
@@ -1,28 +1,36 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
include Rails::Generators::Migration
|
3
|
+
require 'rails/generators/active_record'
|
5
4
|
|
6
|
-
|
5
|
+
module Secretkeeper
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
include ::Rails::Generators::Migration
|
7
8
|
|
8
|
-
|
9
|
-
copy_file "initializer.rb", "config/initializers/secretkeeper.rb"
|
10
|
-
migration_template(
|
11
|
-
"migration.rb",
|
12
|
-
"db/migrate/create_secretkeeper_tables.rb",
|
13
|
-
migration_version: migration_version
|
14
|
-
)
|
15
|
-
route "secretkeeper"
|
16
|
-
end
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
11
|
+
def install
|
12
|
+
copy_file 'initializer.rb', 'config/initializers/secretkeeper.rb'
|
13
|
+
migration_template(
|
14
|
+
'migration.rb',
|
15
|
+
'db/migrate/create_secretkeeper_tables.rb',
|
16
|
+
migration_version: migration_version
|
17
|
+
)
|
18
|
+
route 'secretkeeper'
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.next_migration_number(dirname)
|
22
|
+
next_migration_number = current_migration_number(dirname) + 1
|
23
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def migration_version
|
29
|
+
formatted_version if ::Rails.version >= '5.0.0'
|
30
|
+
end
|
22
31
|
|
23
|
-
|
24
|
-
|
25
|
-
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
32
|
+
def formatted_version
|
33
|
+
"[#{::Rails::VERSION::MAJOR}.#{::Rails::VERSION::MINOR}]"
|
26
34
|
end
|
27
35
|
end
|
28
36
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class CreateSecretkeeperTables < ActiveRecord::Migration<%= migration_version %>
|
2
2
|
def change
|
3
3
|
create_table :secretkeeper_access_tokens do |t|
|
4
|
-
t.references :owner, polymorphic: true, index: true
|
4
|
+
t.references :owner, polymorphic: true, index: true, null: false
|
5
5
|
t.string :token, null: false, index: true, unique: true
|
6
6
|
t.string :refresh_token, null: false, index: true, unique: true
|
7
7
|
t.integer :expires_in
|
data/lib/secretkeeper.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'secretkeeper/rails/router'
|
4
|
+
require 'secretkeeper/access_token'
|
5
|
+
require 'secretkeeper/configuration'
|
6
|
+
require 'secretkeeper/engine'
|
@@ -1,23 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Secretkeeper
|
2
4
|
class AccessToken < ::ActiveRecord::Base
|
3
|
-
self.table_name =
|
5
|
+
self.table_name = 'secretkeeper_access_tokens'
|
6
|
+
|
4
7
|
belongs_to :owner, polymorphic: true
|
5
8
|
|
6
9
|
def initialize(attributes = {})
|
7
|
-
access_token, refresh_token, expires_in = nil
|
8
10
|
loop do
|
9
|
-
access_token = AccessToken.generate
|
10
|
-
|
11
|
-
break unless
|
11
|
+
@access_token = AccessToken.generate
|
12
|
+
@r_token = AccessToken.generate
|
13
|
+
break unless token_exists?
|
12
14
|
end
|
13
|
-
|
14
|
-
super((attributes || {}).merge(
|
15
|
-
token: access_token, refresh_token: refresh_token, expires_in: expires_in
|
16
|
-
}))
|
15
|
+
@token_expires_in = Secretkeeper.configuration.access_token_expires_in
|
16
|
+
super((attributes || {}).merge(token_params))
|
17
17
|
end
|
18
18
|
|
19
19
|
def revoke!
|
20
|
-
revoked_at.nil? &&
|
20
|
+
revoked_at.nil? && update(revoked_at: Time.zone.now)
|
21
21
|
end
|
22
22
|
|
23
23
|
def accessible?
|
@@ -29,7 +29,7 @@ module Secretkeeper
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def expired?
|
32
|
-
created_at + expires_in.seconds <=
|
32
|
+
created_at + expires_in.seconds <= Time.zone.now
|
33
33
|
end
|
34
34
|
|
35
35
|
def refreshable?
|
@@ -39,5 +39,21 @@ module Secretkeeper
|
|
39
39
|
def self.generate
|
40
40
|
SecureRandom.hex(32)
|
41
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_reader :access_token, :r_token, :token_expires_in
|
46
|
+
|
47
|
+
def token_exists?
|
48
|
+
AccessToken.exists?(token: access_token, refresh_token: r_token)
|
49
|
+
end
|
50
|
+
|
51
|
+
def token_params
|
52
|
+
{
|
53
|
+
token: access_token,
|
54
|
+
refresh_token: r_token,
|
55
|
+
expires_in: token_expires_in
|
56
|
+
}
|
57
|
+
end
|
42
58
|
end
|
43
59
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Secretkeeper
|
4
|
+
class << self
|
5
|
+
attr_reader :reflection_resource_owner
|
6
|
+
|
7
|
+
def configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure
|
12
|
+
yield(configuration)
|
13
|
+
end
|
14
|
+
|
15
|
+
def reflect(&block)
|
16
|
+
class_eval(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource_owner(&block)
|
20
|
+
@reflection_resource_owner = block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Configuration
|
25
|
+
attr_accessor :access_token_expires_in
|
26
|
+
end
|
27
|
+
end
|
data/lib/secretkeeper/engine.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'secretkeeper/rails/helpers'
|
2
4
|
|
3
5
|
module Secretkeeper
|
4
6
|
class Engine < ::Rails::Engine
|
5
|
-
initializer
|
7
|
+
initializer 'secretkeeper.controller_helpers' do
|
6
8
|
ActiveSupport.on_load(:action_controller) do
|
7
9
|
include Secretkeeper::Rails::Helpers
|
8
10
|
end
|
@@ -1,17 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Secretkeeper
|
2
4
|
module Rails
|
3
5
|
module Helpers
|
4
6
|
extend ActiveSupport::Concern
|
5
7
|
|
6
8
|
def secretkeeper_authorize!
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
return secretkeeper_render_error unless secretkeeper_token_acceptable?
|
10
|
+
|
11
|
+
@resource_owner = secretkeeper_access_token.owner
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def secretkeeper_token_acceptable?
|
17
|
+
secretkeeper_auth_header_valid? &&
|
18
|
+
secretkeeper_access_token.owner.present?
|
19
|
+
end
|
20
|
+
|
21
|
+
def secretkeeper_auth_header_valid?
|
22
|
+
request.headers['Authorization']&.start_with?('Bearer ') &&
|
23
|
+
secretkeeper_access_token&.accessible?
|
24
|
+
end
|
25
|
+
|
26
|
+
def secretkeeper_access_token
|
27
|
+
@secretkeeper_access_token ||= Secretkeeper::AccessToken.find_by(
|
28
|
+
token: request.headers['Authorization'][7..-1]
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def secretkeeper_render_error
|
15
33
|
head 401
|
16
34
|
end
|
17
35
|
end
|
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ActionDispatch
|
2
4
|
module Routing
|
3
5
|
class Mapper
|
4
6
|
def secretkeeper
|
5
|
-
resources :auth, module:
|
7
|
+
resources :auth, module: 'secretkeeper', only: [] do
|
6
8
|
collection do
|
7
9
|
post :create_token
|
8
10
|
post :refresh_token
|
data/lib/secretkeeper/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: secretkeeper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Korepanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -25,62 +25,88 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: factory_bot
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '5.0'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 5.0.2
|
34
37
|
type: :development
|
35
38
|
prerelease: false
|
36
39
|
version_requirements: !ruby/object:Gem::Requirement
|
37
40
|
requirements:
|
38
41
|
- - "~>"
|
39
42
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
43
|
+
version: '5.0'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 5.0.2
|
41
47
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
48
|
+
name: generator_spec
|
43
49
|
requirement: !ruby/object:Gem::Requirement
|
44
50
|
requirements:
|
45
51
|
- - "~>"
|
46
52
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
53
|
+
version: 0.9.4
|
48
54
|
type: :development
|
49
55
|
prerelease: false
|
50
56
|
version_requirements: !ruby/object:Gem::Requirement
|
51
57
|
requirements:
|
52
58
|
- - "~>"
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
60
|
+
version: 0.9.4
|
55
61
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
62
|
+
name: rspec-json_expectations
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
58
64
|
requirements:
|
59
65
|
- - "~>"
|
60
66
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
67
|
+
version: '2.2'
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: 2.2.0
|
62
71
|
type: :development
|
63
72
|
prerelease: false
|
64
73
|
version_requirements: !ruby/object:Gem::Requirement
|
65
74
|
requirements:
|
66
75
|
- - "~>"
|
67
76
|
- !ruby/object:Gem::Version
|
68
|
-
version: '
|
77
|
+
version: '2.2'
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 2.2.0
|
69
81
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
82
|
+
name: rspec-rails
|
71
83
|
requirement: !ruby/object:Gem::Requirement
|
72
84
|
requirements:
|
73
85
|
- - "~>"
|
74
86
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
87
|
+
version: '3.6'
|
76
88
|
type: :development
|
77
89
|
prerelease: false
|
78
90
|
version_requirements: !ruby/object:Gem::Requirement
|
79
91
|
requirements:
|
80
92
|
- - "~>"
|
81
93
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
83
|
-
|
94
|
+
version: '3.6'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: sqlite3
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - "~>"
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.3'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - "~>"
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '1.3'
|
109
|
+
description: Token based authentication
|
84
110
|
email:
|
85
111
|
- secretkeeper.gem@gmail.com
|
86
112
|
executables: []
|
@@ -96,7 +122,7 @@ files:
|
|
96
122
|
- lib/generators/secretkeeper/templates/migration.rb
|
97
123
|
- lib/secretkeeper.rb
|
98
124
|
- lib/secretkeeper/access_token.rb
|
99
|
-
- lib/secretkeeper/
|
125
|
+
- lib/secretkeeper/configuration.rb
|
100
126
|
- lib/secretkeeper/engine.rb
|
101
127
|
- lib/secretkeeper/rails/helpers.rb
|
102
128
|
- lib/secretkeeper/rails/router.rb
|
@@ -121,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
147
|
version: '0'
|
122
148
|
requirements: []
|
123
149
|
rubyforge_project:
|
124
|
-
rubygems_version: 2.6
|
150
|
+
rubygems_version: 2.7.6
|
125
151
|
signing_key:
|
126
152
|
specification_version: 4
|
127
153
|
summary: Token based authentication
|
data/lib/secretkeeper/config.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
module Secretkeeper
|
2
|
-
class << self
|
3
|
-
attr_accessor :configuration
|
4
|
-
attr_accessor :reflection_resource_owner
|
5
|
-
end
|
6
|
-
|
7
|
-
def self.configuration
|
8
|
-
@configuration ||= Configuration.new
|
9
|
-
end
|
10
|
-
|
11
|
-
def self.configure
|
12
|
-
yield(configuration)
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.reflect(&block)
|
16
|
-
class_eval(&block)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.resource_owner(&block)
|
20
|
-
@reflection_resource_owner = block
|
21
|
-
end
|
22
|
-
|
23
|
-
class Configuration
|
24
|
-
attr_accessor :access_token_expires_in
|
25
|
-
|
26
|
-
def initialize
|
27
|
-
@access_token_expires_in = 1.hour
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|