haveapi 0.12.1 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/protocol.md +27 -10
- data/haveapi.gemspec +1 -2
- data/lib/haveapi/action.rb +23 -7
- data/lib/haveapi/actions/default.rb +3 -3
- data/lib/haveapi/authentication/base.rb +19 -1
- data/lib/haveapi/authentication/basic/provider.rb +7 -1
- data/lib/haveapi/authentication/chain.rb +10 -12
- data/lib/haveapi/authentication/token/action_config.rb +53 -0
- data/lib/haveapi/authentication/token/action_request.rb +23 -0
- data/lib/haveapi/authentication/token/action_result.rb +42 -0
- data/lib/haveapi/authentication/token/config.rb +115 -0
- data/lib/haveapi/authentication/token/provider.rb +259 -81
- data/lib/haveapi/authentication/token.rb +9 -0
- data/lib/haveapi/client_examples/curl.rb +3 -3
- data/lib/haveapi/client_examples/fs_client.rb +3 -3
- data/lib/haveapi/client_examples/http.rb +7 -7
- data/lib/haveapi/client_examples/js_client.rb +1 -1
- data/lib/haveapi/client_examples/php_client.rb +1 -1
- data/lib/haveapi/client_examples/ruby_cli.rb +1 -1
- data/lib/haveapi/client_examples/ruby_client.rb +1 -1
- data/lib/haveapi/context.rb +13 -13
- data/lib/haveapi/example.rb +3 -3
- data/lib/haveapi/exceptions.rb +2 -0
- data/lib/haveapi/extensions/exception_mailer.rb +8 -1
- data/lib/haveapi/model_adapters/active_record.rb +6 -6
- data/lib/haveapi/parameters/resource.rb +10 -10
- data/lib/haveapi/params.rb +1 -1
- data/lib/haveapi/resource.rb +18 -10
- data/lib/haveapi/resources/action_state.rb +2 -2
- data/lib/haveapi/route.rb +4 -3
- data/lib/haveapi/server.rb +7 -7
- data/lib/haveapi/spec/mock_action.rb +3 -3
- data/lib/haveapi/spec/spec_methods.rb +8 -8
- data/lib/haveapi/version.rb +2 -2
- data/lib/haveapi/views/version_page.erb +2 -2
- data/shell.nix +1 -1
- metadata +9 -5
- data/lib/haveapi/authentication/token/resources.rb +0 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e44e03fec908d742be4499f22c97f16453cd53bd4a23a14928af94d3456bea2
|
4
|
+
data.tar.gz: '0339228a0003baae1528050052b930acedbad51651893e4ce8329945bf98a5f0'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 157bfeeaf24ea9304e96051c4a05e9454cdfbb818ac7476efbb7e587547305bbd1cb54bb9acfebf0f7ac3a8b553766b7527fa80a8af575d306df051e46394be6
|
7
|
+
data.tar.gz: 1dbfe70f5b5e7f15c1cc779df344a09afcc0912df4a74ac27ff51f45f65f3eef574dd66ff0fee80c7f6b44f093491c0242494fbd52af7b46a1674ad3971f1933
|
data/doc/protocol.md
CHANGED
@@ -94,14 +94,29 @@ HTTP basic authentication needs no other configuration, only informs about its p
|
|
94
94
|
|
95
95
|
"basic": {}
|
96
96
|
|
97
|
+
HTTP basic authentication does not support multi-factor authentication.
|
98
|
+
|
97
99
|
### Token authentication
|
98
|
-
Token authentication contains
|
99
|
-
|
100
|
+
Token authentication contains resource ``token``, that is used to acquire
|
101
|
+
and revoke tokens.
|
102
|
+
|
103
|
+
Tokens are acquired by action ``request``, in which the client provides arbitrary
|
104
|
+
login credentials. If the login credentials match, the server either concludes
|
105
|
+
the authentication process, or multiple authentication steps may be necessary.
|
106
|
+
|
107
|
+
For single-step authentication processses, action ``request`` returns the
|
108
|
+
token and its validity period. If multiple authentication steps are necessary,
|
109
|
+
the server signals this by returning ``complete = true``. The client then has
|
110
|
+
to call the next authentication step, which is an action on the ``token``
|
111
|
+
resource, as returned in ``next_action``.
|
100
112
|
|
101
|
-
|
102
|
-
|
113
|
+
Tokens are returned in both cases. If the authentication is finished, the token
|
114
|
+
is then used for authenticating further requests. If the authentication continues,
|
115
|
+
the token is used to authenticate the next authentication request, which then
|
116
|
+
returns another token.
|
103
117
|
|
104
|
-
|
118
|
+
After the authentication is complete, acquired tokens can be renewed using action
|
119
|
+
``renew`` and revoked by calling the ``revoke`` action.
|
105
120
|
|
106
121
|
"token": {
|
107
122
|
"http_header": "<name of HTTP header to transfer token in, by default X-HaveAPI-Auth-Token>",
|
@@ -113,7 +128,7 @@ Token can be revoked by calling the ``revoke`` action.
|
|
113
128
|
"input": {
|
114
129
|
...
|
115
130
|
"parameters": {
|
116
|
-
"
|
131
|
+
"user": ...
|
117
132
|
"password": ...
|
118
133
|
"lifetime": ...
|
119
134
|
"interval": ...
|
@@ -125,6 +140,8 @@ Token can be revoked by calling the ``revoke`` action.
|
|
125
140
|
"parameters": {
|
126
141
|
"token": ...
|
127
142
|
"valid_to": ...
|
143
|
+
"complete": true/false
|
144
|
+
"next_action": ...
|
128
145
|
},
|
129
146
|
...
|
130
147
|
},
|
@@ -176,7 +193,7 @@ Every action is described as:
|
|
176
193
|
... list of examples ...
|
177
194
|
],
|
178
195
|
"meta": ... metadata ...,
|
179
|
-
"
|
196
|
+
"path": "URL for this action",
|
180
197
|
"method": "HTTP method to be used",
|
181
198
|
"help": "URL to get this very description of the action"
|
182
199
|
}
|
@@ -365,12 +382,12 @@ This is used for associations between resources, e.g. car has a wheel.
|
|
365
382
|
"value_id": "<name of a parameter that is used as an id>",
|
366
383
|
"value_label": "<name of a parameter that is used as a value>",
|
367
384
|
"value": {
|
368
|
-
"
|
385
|
+
"path": "URL to 'show' action of associated resource",
|
369
386
|
"method": "HTTP method to use",
|
370
387
|
"help": "URL to get the associated resource's 'show' description"
|
371
388
|
},
|
372
389
|
"choices": {
|
373
|
-
"
|
390
|
+
"path": "URL to action that returns a list of possible associations",
|
374
391
|
"method": "HTTP method to use",
|
375
392
|
"help": "URL to description of the list action"
|
376
393
|
}
|
@@ -391,7 +408,7 @@ render them according to its syntax.
|
|
391
408
|
|
392
409
|
{
|
393
410
|
"title": "A title",
|
394
|
-
"
|
411
|
+
"path_params: [ ... array of integers ... ],
|
395
412
|
"request": {
|
396
413
|
... a hash of request parameters ...
|
397
414
|
},
|
data/haveapi.gemspec
CHANGED
@@ -5,7 +5,6 @@ require 'haveapi/version'
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'haveapi'
|
7
7
|
s.version = HaveAPI::VERSION
|
8
|
-
s.date = '2017-11-27'
|
9
8
|
s.summary =
|
10
9
|
s.description = 'Framework for creating self-describing APIs'
|
11
10
|
s.authors = 'Jakub Skokan'
|
@@ -24,6 +23,6 @@ Gem::Specification.new do |s|
|
|
24
23
|
s.add_runtime_dependency 'rake'
|
25
24
|
s.add_runtime_dependency 'github-markdown'
|
26
25
|
s.add_runtime_dependency 'nesty', '~> 1.0'
|
27
|
-
s.add_runtime_dependency 'haveapi-client', '~> 0.
|
26
|
+
s.add_runtime_dependency 'haveapi-client', '~> 0.13.0'
|
28
27
|
s.add_runtime_dependency 'mail'
|
29
28
|
end
|
data/lib/haveapi/action.rb
CHANGED
@@ -31,7 +31,8 @@ module HaveAPI
|
|
31
31
|
attr_accessor :flags
|
32
32
|
|
33
33
|
class << self
|
34
|
-
|
34
|
+
attr_accessor :resource
|
35
|
+
attr_reader :authorization, :examples
|
35
36
|
|
36
37
|
def inherited(subclass)
|
37
38
|
# puts "Action.inherited called #{subclass} from #{to_s}"
|
@@ -45,7 +46,7 @@ module HaveAPI
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def delayed_inherited(subclass)
|
48
|
-
resource = Kernel.const_get(subclass.to_s.deconstantize)
|
49
|
+
resource = subclass.resource || Kernel.const_get(subclass.to_s.deconstantize)
|
49
50
|
|
50
51
|
inherit_attrs(subclass)
|
51
52
|
inherit_attrs_from_resource(subclass, resource, [:auth])
|
@@ -177,14 +178,29 @@ module HaveAPI
|
|
177
178
|
@examples << e
|
178
179
|
end
|
179
180
|
|
181
|
+
def action_name
|
182
|
+
(@action_name ? @action_name.to_s : to_s).demodulize
|
183
|
+
end
|
184
|
+
|
185
|
+
def action_name=(name)
|
186
|
+
@action_name = name
|
187
|
+
end
|
188
|
+
|
180
189
|
def build_route(prefix)
|
181
|
-
route = @route ||
|
190
|
+
route = @route || action_name.underscore
|
191
|
+
if @route
|
192
|
+
@route
|
193
|
+
elsif action_name
|
194
|
+
action_name.to_s.demodulize.underscore
|
195
|
+
else
|
196
|
+
to_s.demodulize.underscore
|
197
|
+
end
|
182
198
|
|
183
199
|
if !route.is_a?(String) && route.respond_to?(:call)
|
184
200
|
route = route.call(self.resource)
|
185
201
|
end
|
186
202
|
|
187
|
-
prefix + route % {resource: self.resource.
|
203
|
+
prefix + route % {resource: self.resource.resource_name.underscore}
|
188
204
|
end
|
189
205
|
|
190
206
|
def describe(context)
|
@@ -214,9 +230,9 @@ module HaveAPI
|
|
214
230
|
output: @output ? @output.describe(context) : {parameters: {}},
|
215
231
|
meta: @meta ? @meta.merge(@meta) { |_, v| v && v.describe(context) } : nil,
|
216
232
|
examples: @examples ? @examples.describe(context) : [],
|
217
|
-
|
233
|
+
path: context.resolved_path,
|
218
234
|
method: route_method,
|
219
|
-
help: "#{context.
|
235
|
+
help: "#{context.path}?method=#{route_method}"
|
220
236
|
}
|
221
237
|
end
|
222
238
|
|
@@ -245,7 +261,7 @@ module HaveAPI
|
|
245
261
|
ret
|
246
262
|
end
|
247
263
|
|
248
|
-
def
|
264
|
+
def resolve_path_params(object)
|
249
265
|
if self.resolve
|
250
266
|
self.resolve.call(object)
|
251
267
|
|
@@ -38,18 +38,18 @@ module HaveAPI
|
|
38
38
|
end
|
39
39
|
|
40
40
|
class Show < Action
|
41
|
-
route ->(r){ r.singular ? '' : '
|
41
|
+
route ->(r){ r.singular ? '' : '{%{resource}_id}' }
|
42
42
|
http_method :get
|
43
43
|
aliases %i(find)
|
44
44
|
end
|
45
45
|
|
46
46
|
class Update < Action
|
47
|
-
route ->(r){ r.singular ? '' : '
|
47
|
+
route ->(r){ r.singular ? '' : '{%{resource}_id}' }
|
48
48
|
http_method :put
|
49
49
|
end
|
50
50
|
|
51
51
|
class Delete < Action
|
52
|
-
route ->(r){ r.singular ? '' : '
|
52
|
+
route ->(r){ r.singular ? '' : '{%{resource}_id}' }
|
53
53
|
http_method :delete
|
54
54
|
aliases %i(destroy)
|
55
55
|
end
|
@@ -2,14 +2,32 @@ module HaveAPI
|
|
2
2
|
module Authentication
|
3
3
|
# Base class for authentication providers.
|
4
4
|
class Base
|
5
|
-
|
5
|
+
# Get or set auth method name
|
6
|
+
# @param v [Symbol, nil]
|
7
|
+
# @return [Symbol]
|
8
|
+
def self.auth_method(v = nil)
|
9
|
+
if v
|
10
|
+
@auth_method = v
|
11
|
+
else
|
12
|
+
@auth_method || name.split('::').last.underscore.to_sym
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Symbol]
|
17
|
+
attr_reader :name
|
6
18
|
|
7
19
|
def initialize(server, v)
|
20
|
+
@name = self.class.auth_method
|
8
21
|
@server = server
|
9
22
|
@version = v
|
10
23
|
setup
|
11
24
|
end
|
12
25
|
|
26
|
+
# @return [Module, nil]
|
27
|
+
def resource_module
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
13
31
|
# Reimplement this method in your authentication provider.
|
14
32
|
# +request+ is passed directly from Sinatra.
|
15
33
|
def authenticate(request)
|
@@ -17,12 +17,18 @@ module HaveAPI::Authentication
|
|
17
17
|
# ...
|
18
18
|
# api.auth_chain << MyBasicAuth
|
19
19
|
class Provider < Base
|
20
|
+
auth_method :basic
|
21
|
+
|
20
22
|
def authenticate(request)
|
21
23
|
user = nil
|
22
24
|
|
23
25
|
auth = Rack::Auth::Basic::Request.new(request.env)
|
24
26
|
if auth.provided? && auth.basic? && auth.credentials
|
25
|
-
|
27
|
+
begin
|
28
|
+
user = find_user(request, *auth.credentials)
|
29
|
+
rescue HaveAPI::AuthenticationError
|
30
|
+
user = nil
|
31
|
+
end
|
26
32
|
end
|
27
33
|
|
28
34
|
user
|
@@ -50,17 +50,17 @@ module HaveAPI::Authentication
|
|
50
50
|
|
51
51
|
def describe(context)
|
52
52
|
ret = {}
|
53
|
-
|
53
|
+
|
54
54
|
return ret unless @instances[context.version]
|
55
55
|
|
56
56
|
@instances[context.version].each do |provider|
|
57
57
|
ret[provider.name] = provider.describe
|
58
58
|
|
59
|
-
if provider.
|
59
|
+
if provider.resource_module
|
60
60
|
ret[provider.name][:resources] = {}
|
61
61
|
|
62
62
|
@server.routes[context.version][:authentication][provider.name][:resources].each do |r, children|
|
63
|
-
ret[provider.name][:resources][r.
|
63
|
+
ret[provider.name][:resources][r.resource_name.underscore.to_sym] = r.describe(children, context)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
@@ -97,17 +97,15 @@ module HaveAPI::Authentication
|
|
97
97
|
protected
|
98
98
|
def register_provider(v, p)
|
99
99
|
instance = p.new(@server, v)
|
100
|
-
parts = p.superclass.name.split('::')
|
101
|
-
|
102
|
-
instance.name = parts[-2].underscore.to_sym
|
103
|
-
|
104
100
|
@instances[v] << instance
|
105
101
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
102
|
+
if resource_module = instance.resource_module
|
103
|
+
@server.add_auth_module(
|
104
|
+
v,
|
105
|
+
instance.name,
|
106
|
+
resource_module,
|
107
|
+
prefix: instance.name.to_s,
|
108
|
+
)
|
111
109
|
end
|
112
110
|
end
|
113
111
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Token
|
3
|
+
class ActionConfig
|
4
|
+
# @param block [Proc]
|
5
|
+
# @param opts [Hash]
|
6
|
+
# @option opts [Boolean] :input
|
7
|
+
# @option opts [Boolean] :handle
|
8
|
+
def initialize(block, opts = {})
|
9
|
+
@block = block
|
10
|
+
@opts = with_defaults(opts)
|
11
|
+
update(block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param block [Proc]
|
15
|
+
def update(block)
|
16
|
+
instance_exec(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Configure input parameters in the context of {HaveAPI::Params}
|
20
|
+
def input(&block)
|
21
|
+
if block && check!(:input)
|
22
|
+
@input = block
|
23
|
+
else
|
24
|
+
@input
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Handle the action
|
29
|
+
# @yieldparam request [ActionRequest]
|
30
|
+
# @yieldparam result [ActionResult]
|
31
|
+
# @yieldreturn [ActionResult]
|
32
|
+
def handle(&block)
|
33
|
+
if block && check!(:handle)
|
34
|
+
@handle = block
|
35
|
+
else
|
36
|
+
@handle
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def check!(name)
|
42
|
+
fail "#{name} cannot be configured" unless @opts[name]
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_defaults(opts)
|
47
|
+
Hash[%i(input handle).map do |v|
|
48
|
+
[v, opts.has_key?(v) ? opts[v] : true]
|
49
|
+
end]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Token
|
3
|
+
class ActionRequest
|
4
|
+
# @return [Sinatra::Request]
|
5
|
+
attr_reader :request
|
6
|
+
|
7
|
+
# @return [Hash]
|
8
|
+
attr_reader :input
|
9
|
+
|
10
|
+
# @return [Object, nil]
|
11
|
+
attr_reader :user
|
12
|
+
|
13
|
+
# @return [String, nil]
|
14
|
+
attr_reader :token
|
15
|
+
|
16
|
+
def initialize(opts = {})
|
17
|
+
opts.each do |k, v|
|
18
|
+
instance_variable_set(:"@#{k}", v)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Token
|
3
|
+
class ActionResult
|
4
|
+
# @param complete [Boolean]
|
5
|
+
# @return [Boolean]
|
6
|
+
attr_accessor :complete
|
7
|
+
|
8
|
+
# @param error [String]
|
9
|
+
# @return [String, nil]
|
10
|
+
attr_accessor :error
|
11
|
+
|
12
|
+
# @param token [String]
|
13
|
+
# @return [String, nil]
|
14
|
+
attr_accessor :token
|
15
|
+
|
16
|
+
# @param valid_to [Time]
|
17
|
+
# @return [Time, nil]
|
18
|
+
attr_accessor :valid_to
|
19
|
+
|
20
|
+
# @param next_action [String]
|
21
|
+
# @return [String, nil]
|
22
|
+
attr_accessor :next_action
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@ok = false
|
26
|
+
end
|
27
|
+
|
28
|
+
def ok
|
29
|
+
@ok = true
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def ok?
|
34
|
+
@ok && @error.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def complete?
|
38
|
+
@complete ? true : false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Token
|
3
|
+
# Configuration for {HaveAPI::Authentication::Token::Provider}
|
4
|
+
#
|
5
|
+
# Create a subclass and use with {HaveAPI::Authentication::Token#with_config}.
|
6
|
+
class Config
|
7
|
+
class << self
|
8
|
+
# Configure token request action
|
9
|
+
def request(&block)
|
10
|
+
if block
|
11
|
+
if @request
|
12
|
+
@request.update(block)
|
13
|
+
else
|
14
|
+
@request = ActionConfig.new(block)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
@request
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
%i(renew revoke).each do |name|
|
22
|
+
# Configuration method
|
23
|
+
define_method(name) do |&block|
|
24
|
+
var = :"@#{name}"
|
25
|
+
val = instance_variable_get(var)
|
26
|
+
|
27
|
+
if block
|
28
|
+
if val
|
29
|
+
val.update(block)
|
30
|
+
else
|
31
|
+
instance_variable_set(var, ActionConfig.new(block, input: false))
|
32
|
+
end
|
33
|
+
else
|
34
|
+
val
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param name [Symbol]
|
40
|
+
def action(name, &block)
|
41
|
+
@actions ||= {}
|
42
|
+
@actions[name] = ActionConfig.new(block)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Hash]
|
46
|
+
def actions
|
47
|
+
@actions || {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# HTTP header that is searched for token
|
51
|
+
# @param header [String, nil]
|
52
|
+
# @return [String]
|
53
|
+
def http_header(header = nil)
|
54
|
+
if header
|
55
|
+
@http_header = header
|
56
|
+
else
|
57
|
+
@http_header || 'X-HaveAPI-Auth-Token'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Query parameter searched for token
|
62
|
+
# @param param [Symbol]
|
63
|
+
# @return [Symbol]
|
64
|
+
def query_parameter(param = nil)
|
65
|
+
if param
|
66
|
+
@query_param = param
|
67
|
+
else
|
68
|
+
@query_param || :_auth_token
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def inherited(subclass)
|
73
|
+
# Default request
|
74
|
+
subclass.request do
|
75
|
+
input do
|
76
|
+
string :user, label: 'User', required: true
|
77
|
+
password :password, label: 'Password', required: true
|
78
|
+
end
|
79
|
+
|
80
|
+
handle do
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Default renew and revoke
|
86
|
+
%i(renew revoke).each do |name|
|
87
|
+
subclass.send(name) do
|
88
|
+
handle do
|
89
|
+
raise NotImplementedError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize(server, v)
|
97
|
+
@server = server
|
98
|
+
@version = v
|
99
|
+
end
|
100
|
+
|
101
|
+
# Authenticate request by `token`
|
102
|
+
#
|
103
|
+
# Return user object or nil.
|
104
|
+
# If the token was created as auto-renewable, this method
|
105
|
+
# is responsible for its renewal.
|
106
|
+
# Must be implemented.
|
107
|
+
# @param request [Sinatra::Request]
|
108
|
+
# @param token [String]
|
109
|
+
# @return [Object, nil]
|
110
|
+
def find_user_by_token(request, token)
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|