clowk 0.1.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 +7 -0
- data/README.md +365 -0
- data/clowk.gemspec +31 -0
- data/config/routes.rb +8 -0
- data/lib/clowk/authenticable.rb +112 -0
- data/lib/clowk/configuration.rb +46 -0
- data/lib/clowk/controllers/base_controller.rb +70 -0
- data/lib/clowk/controllers/callbacks_controller.rb +25 -0
- data/lib/clowk/controllers/sessions_controller.rb +23 -0
- data/lib/clowk/current.rb +47 -0
- data/lib/clowk/engine.rb +17 -0
- data/lib/clowk/helpers/url_helpers.rb +58 -0
- data/lib/clowk/http/client.rb +199 -0
- data/lib/clowk/http/logger_middleware.rb +29 -0
- data/lib/clowk/http/response.rb +43 -0
- data/lib/clowk/http/retry_middleware.rb +47 -0
- data/lib/clowk/http/timeout_middleware.rb +30 -0
- data/lib/clowk/jwt_verifier.rb +27 -0
- data/lib/clowk/middleware/token_extractor.rb +42 -0
- data/lib/clowk/sdk/client.rb +89 -0
- data/lib/clowk/sdk/resource.rb +68 -0
- data/lib/clowk/sdk/session.rb +11 -0
- data/lib/clowk/sdk/subdomain.rb +17 -0
- data/lib/clowk/sdk/token.rb +15 -0
- data/lib/clowk/sdk/user.rb +11 -0
- data/lib/clowk/subdomain.rb +120 -0
- data/lib/clowk/version.rb +5 -0
- data/lib/clowk.rb +56 -0
- metadata +136 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
class Current
|
|
5
|
+
attr_reader :attributes
|
|
6
|
+
|
|
7
|
+
def initialize(attributes = {})
|
|
8
|
+
@attributes = attributes.to_h.deep_symbolize_keys
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def id
|
|
12
|
+
attributes[:sub] || attributes[:id]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def email
|
|
16
|
+
attributes[:email]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def name
|
|
20
|
+
attributes[:name]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def avatar_url
|
|
24
|
+
attributes[:avatar_url]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def provider
|
|
28
|
+
attributes[:provider]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def instance_id
|
|
32
|
+
attributes[:instance_id]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def app_id
|
|
36
|
+
attributes[:app_id]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def [](key)
|
|
40
|
+
attributes[key.to_sym]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
attributes.merge(id: id)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/clowk/engine.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Clowk
|
|
6
|
+
|
|
7
|
+
initializer 'clowk.helpers' do
|
|
8
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
9
|
+
include Clowk::Helpers::UrlHelpers
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
ActiveSupport.on_load(:action_view) do
|
|
13
|
+
include Clowk::Helpers::UrlHelpers
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cgi'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
module Helpers
|
|
7
|
+
module UrlHelpers
|
|
8
|
+
def clowk_sign_in_path(return_to: nil)
|
|
9
|
+
append_query(clowk_local_path('/sign_in'), return_to:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clowk_sign_up_path(return_to: nil)
|
|
13
|
+
append_query(clowk_local_path('/sign_up'), return_to:)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def clowk_sign_out_path(return_to: nil)
|
|
17
|
+
append_query(clowk_local_path('/sign_out'), return_to:)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def clowk_callback_url(return_to: nil, state: nil)
|
|
21
|
+
append_query("#{request.base_url}#{Clowk.config.callback_path}", return_to:, state:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clowk_sign_in_url(redirect_to: nil, return_to: nil, state: nil)
|
|
25
|
+
clowk_remote_auth_url('sign-in', redirect_to:, return_to:, state:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clowk_sign_up_url(redirect_to: nil, return_to: nil, state: nil)
|
|
29
|
+
clowk_remote_auth_url('sign-up', redirect_to:, return_to:, state:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def clowk_remote_auth_url(action, redirect_to:, return_to:, state:)
|
|
35
|
+
callback_url = clowk_callback_url(return_to: redirect_to || return_to, state:)
|
|
36
|
+
query = { redirect_uri: callback_url }
|
|
37
|
+
|
|
38
|
+
append_query("#{clowk_instance_base_url}/#{action}", query)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clowk_instance_base_url
|
|
42
|
+
Clowk::Subdomain.resolve_url!
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clowk_local_path(path)
|
|
46
|
+
"#{Clowk.config.mount_path}#{path}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def append_query(url, params = {})
|
|
50
|
+
filtered = params.compact.reject { |_key, value| value.respond_to?(:empty?) && value.empty? }
|
|
51
|
+
return url if filtered.empty?
|
|
52
|
+
|
|
53
|
+
separator = url.include?('?') ? '&' : '?'
|
|
54
|
+
"#{url}#{separator}#{Rack::Utils.build_query(filtered)}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module Clowk
|
|
8
|
+
class Http
|
|
9
|
+
MAX_BODY_SIZE = 1 * 1024 * 1024
|
|
10
|
+
|
|
11
|
+
HTTP_METHODS = {
|
|
12
|
+
get: Net::HTTP::Get,
|
|
13
|
+
post: Net::HTTP::Post,
|
|
14
|
+
put: Net::HTTP::Put,
|
|
15
|
+
patch: Net::HTTP::Patch,
|
|
16
|
+
delete: Net::HTTP::Delete,
|
|
17
|
+
head: Net::HTTP::Head,
|
|
18
|
+
options: Net::HTTP::Options
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def self.get(base_url:, path:, headers: {}, logger: nil)
|
|
22
|
+
new(base_url:, headers:, logger:).get(path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.post(base_url:, path:, body: nil, headers: {}, logger: nil)
|
|
26
|
+
new(base_url:, headers:, logger:).post(path, body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.put(base_url:, path:, body: nil, headers: {}, logger: nil)
|
|
30
|
+
new(base_url:, headers:, logger:).put(path, body)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.patch(base_url:, path:, body: nil, headers: {}, logger: nil)
|
|
34
|
+
new(base_url:, headers:, logger:).patch(path, body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.delete(base_url:, path:, body: nil, headers: {}, logger: nil)
|
|
38
|
+
new(base_url:, headers:, logger:).delete(path, body)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.head(base_url:, path:, headers: {}, logger: nil)
|
|
42
|
+
new(base_url:, headers:, logger:).head(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.options(base_url:, path:, headers: {}, logger: nil)
|
|
46
|
+
new(base_url:, headers:, logger:).options(path)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def initialize(base_url:, headers: {}, logger: nil, open_timeout: 5, read_timeout: 10, write_timeout: 10, retry_attempts: 2, retry_interval: 0.05, middlewares: nil)
|
|
50
|
+
@base_url = base_url
|
|
51
|
+
@headers = headers
|
|
52
|
+
@logger = logger
|
|
53
|
+
@open_timeout = open_timeout
|
|
54
|
+
@read_timeout = read_timeout
|
|
55
|
+
@write_timeout = write_timeout
|
|
56
|
+
@retry_attempts = retry_attempts
|
|
57
|
+
@retry_interval = retry_interval
|
|
58
|
+
@middlewares = middlewares || [TimeoutMiddleware, RetryMiddleware, LoggerMiddleware]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get(path, headers: {})
|
|
62
|
+
request(:get, path, headers:)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def post(path, body = nil, headers: {})
|
|
66
|
+
request(:post, path, body:, headers:)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def put(path, body = nil, headers: {})
|
|
70
|
+
request(:put, path, body:, headers:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def patch(path, body = nil, headers: {})
|
|
74
|
+
request(:patch, path, body:, headers:)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def delete(path, body = nil, headers: {})
|
|
78
|
+
request(:delete, path, body:, headers:)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def head(path, headers: {})
|
|
82
|
+
request(:head, path, headers:)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def options(path, headers: {})
|
|
86
|
+
request(:options, path, headers:)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def request(method, path, body: nil, headers: {})
|
|
90
|
+
env = {
|
|
91
|
+
method: method.to_sym,
|
|
92
|
+
uri: build_uri(path),
|
|
93
|
+
body: body,
|
|
94
|
+
headers: headers,
|
|
95
|
+
open_timeout: open_timeout,
|
|
96
|
+
read_timeout: read_timeout,
|
|
97
|
+
write_timeout: write_timeout,
|
|
98
|
+
retry_attempts: retry_attempts,
|
|
99
|
+
retry_interval: retry_interval
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
build_stack.call(env)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
attr_reader :base_url, :headers, :logger, :middlewares, :open_timeout, :read_timeout, :write_timeout, :retry_attempts, :retry_interval
|
|
108
|
+
|
|
109
|
+
def build_stack
|
|
110
|
+
app = lambda { |env| perform_request(env) }
|
|
111
|
+
|
|
112
|
+
middlewares.reverse.inject(app) do |next_app, middleware|
|
|
113
|
+
middleware.new(next_app, logger:, open_timeout:, read_timeout:, write_timeout:)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def perform_request(env)
|
|
118
|
+
request = request_class_for(env[:method]).new(env[:uri])
|
|
119
|
+
|
|
120
|
+
apply_headers(request, env[:headers])
|
|
121
|
+
|
|
122
|
+
request.body = JSON.generate(env[:body]) unless env[:body].nil?
|
|
123
|
+
|
|
124
|
+
raw_response = Net::HTTP.start(env[:uri].host, env[:uri].port, use_ssl: env[:uri].scheme == 'https') do |http|
|
|
125
|
+
apply_timeouts(http, env[:timeouts])
|
|
126
|
+
http.request(request)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
parse_response(raw_response)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def request_class_for(method)
|
|
133
|
+
HTTP_METHODS.fetch(method.to_sym) do
|
|
134
|
+
raise ArgumentError, "unsupported HTTP method: #{method}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_uri(path)
|
|
139
|
+
base_uri = URI(base_url)
|
|
140
|
+
base_uri.path = join_paths(base_uri.path, normalize_path(path))
|
|
141
|
+
base_uri.query = nil
|
|
142
|
+
base_uri.fragment = nil
|
|
143
|
+
base_uri
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def normalize_path(path)
|
|
147
|
+
path.to_s.start_with?('/') ? path : "/#{path}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def join_paths(base_path, extra_path)
|
|
151
|
+
segments = [base_path.to_s, extra_path.to_s].map { |segment| segment.gsub(%r{^/+|/+$}, '') }.reject(&:empty?)
|
|
152
|
+
"/#{segments.join('/')}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def apply_headers(request, request_headers)
|
|
156
|
+
merged_headers = default_headers.merge(headers).merge(request_headers)
|
|
157
|
+
merged_headers.each { |key, value| request[key] = value }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def apply_timeouts(http, timeouts)
|
|
161
|
+
return unless timeouts
|
|
162
|
+
|
|
163
|
+
http.open_timeout = timeouts[:open_timeout] if timeouts.key?(:open_timeout)
|
|
164
|
+
http.read_timeout = timeouts[:read_timeout] if timeouts.key?(:read_timeout)
|
|
165
|
+
http.write_timeout = timeouts[:write_timeout] if timeouts.key?(:write_timeout) && http.respond_to?(:write_timeout=)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def default_headers
|
|
169
|
+
{
|
|
170
|
+
'Accept' => 'application/json',
|
|
171
|
+
'Content-Type' => 'application/json'
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_response(response)
|
|
176
|
+
body = response.body.to_s
|
|
177
|
+
|
|
178
|
+
raise Error, "response body too large (#{body.bytesize} bytes, max #{MAX_BODY_SIZE})" if body.bytesize > MAX_BODY_SIZE
|
|
179
|
+
|
|
180
|
+
parsed = parse_body(body)
|
|
181
|
+
|
|
182
|
+
Response.new(
|
|
183
|
+
status: response.code.to_i,
|
|
184
|
+
body: body,
|
|
185
|
+
body_parsed: parsed,
|
|
186
|
+
headers: response.to_hash,
|
|
187
|
+
success: response.is_a?(Net::HTTPSuccess)
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def parse_body(body)
|
|
192
|
+
return {} if body.empty?
|
|
193
|
+
|
|
194
|
+
JSON.parse(body)
|
|
195
|
+
rescue JSON::ParserError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
class Http
|
|
7
|
+
class LoggerMiddleware
|
|
8
|
+
def initialize(app, logger: nil, **)
|
|
9
|
+
@app = app
|
|
10
|
+
@logger = logger || NullLogger.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
logger.info("[Clowk::Http] #{env[:method].upcase} #{env[:uri]}")
|
|
15
|
+
response = app.call(env)
|
|
16
|
+
logger.info("[Clowk::Http] -> #{response[:status]}")
|
|
17
|
+
response
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :app, :logger
|
|
23
|
+
|
|
24
|
+
class NullLogger
|
|
25
|
+
def info(*); end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
class Http
|
|
5
|
+
class Response
|
|
6
|
+
attr_reader :body, :body_parsed, :headers, :status
|
|
7
|
+
|
|
8
|
+
def initialize(status:, body:, body_parsed:, headers:, success:)
|
|
9
|
+
@status = status
|
|
10
|
+
@body = body
|
|
11
|
+
@body_parsed = body_parsed
|
|
12
|
+
@headers = headers
|
|
13
|
+
@success = success
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def success?
|
|
17
|
+
@success
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def [](key)
|
|
21
|
+
to_h.fetch(key)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
status: status,
|
|
27
|
+
body: body,
|
|
28
|
+
body_parsed: body_parsed,
|
|
29
|
+
headers: headers,
|
|
30
|
+
success?: success?
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
if other.respond_to?(:to_h)
|
|
36
|
+
to_h == other.to_h
|
|
37
|
+
else
|
|
38
|
+
to_h == other
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
class Http
|
|
7
|
+
class RetryMiddleware
|
|
8
|
+
RETRYABLE_ERRORS = [
|
|
9
|
+
EOFError,
|
|
10
|
+
Errno::ECONNRESET,
|
|
11
|
+
Errno::ETIMEDOUT,
|
|
12
|
+
IOError,
|
|
13
|
+
Net::OpenTimeout,
|
|
14
|
+
Net::ReadTimeout,
|
|
15
|
+
Net::WriteTimeout,
|
|
16
|
+
SocketError
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(app, logger: nil, **)
|
|
20
|
+
@app = app
|
|
21
|
+
@logger = logger || LoggerMiddleware::NullLogger.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
attempts = env.fetch(:retry_attempts, 0)
|
|
26
|
+
interval = env.fetch(:retry_interval, 0.0)
|
|
27
|
+
current_attempt = 0
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
current_attempt += 1
|
|
31
|
+
env[:attempt] = current_attempt
|
|
32
|
+
app.call(env)
|
|
33
|
+
rescue *RETRYABLE_ERRORS => error
|
|
34
|
+
raise error if current_attempt > attempts
|
|
35
|
+
|
|
36
|
+
logger.info("[Clowk::Http] retry=#{current_attempt} error=#{error.class}")
|
|
37
|
+
sleep(interval) if interval.positive?
|
|
38
|
+
retry
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :app, :logger
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
class Http
|
|
5
|
+
class TimeoutMiddleware
|
|
6
|
+
def initialize(app, logger: nil, open_timeout: nil, read_timeout: nil, write_timeout: nil, **)
|
|
7
|
+
@app = app
|
|
8
|
+
@logger = logger || LoggerMiddleware::NullLogger.new
|
|
9
|
+
@open_timeout = open_timeout
|
|
10
|
+
@read_timeout = read_timeout
|
|
11
|
+
@write_timeout = write_timeout
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
env[:timeouts] = {
|
|
16
|
+
open_timeout: env.fetch(:open_timeout, open_timeout),
|
|
17
|
+
read_timeout: env.fetch(:read_timeout, read_timeout),
|
|
18
|
+
write_timeout: env.fetch(:write_timeout, write_timeout)
|
|
19
|
+
}.compact
|
|
20
|
+
|
|
21
|
+
logger.info("[Clowk::Http] timeouts=#{env[:timeouts]}") unless env[:timeouts].empty?
|
|
22
|
+
app.call(env)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :app, :logger, :open_timeout, :read_timeout, :write_timeout
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'jwt'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
class JwtVerifier
|
|
7
|
+
ALGORITHM = 'HS256'
|
|
8
|
+
|
|
9
|
+
def initialize(secret_key: Clowk.config.secret_key, issuer: Clowk.config.issuer)
|
|
10
|
+
@secret_key = secret_key
|
|
11
|
+
@issuer = issuer
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def verify(token)
|
|
15
|
+
raise ConfigurationError, 'missing Clowk secret_key' if @secret_key.to_s.empty?
|
|
16
|
+
|
|
17
|
+
options = { algorithm: ALGORITHM }
|
|
18
|
+
options[:iss] = @issuer if @issuer
|
|
19
|
+
options[:verify_iss] = @issuer.present?
|
|
20
|
+
|
|
21
|
+
payload, = JWT.decode(token, @secret_key, true, options)
|
|
22
|
+
payload.deep_symbolize_keys
|
|
23
|
+
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature, JWT::InvalidIssuerError => e
|
|
24
|
+
raise InvalidTokenError, e.message
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clowk
|
|
4
|
+
module Middleware
|
|
5
|
+
class TokenExtractor
|
|
6
|
+
def initialize(request, token_param: Clowk.config.token_param, cookie_key: Clowk.config.cookie_key)
|
|
7
|
+
@request = request
|
|
8
|
+
@token_param = token_param
|
|
9
|
+
@cookie_key = cookie_key
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
token_from_params || token_from_bearer || token_from_cookies
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :request, :token_param, :cookie_key
|
|
19
|
+
|
|
20
|
+
def token_from_params
|
|
21
|
+
params = request.respond_to?(:params) && request.params ? request.params : {}
|
|
22
|
+
params[token_param.to_s].presence
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def token_from_bearer
|
|
26
|
+
header = request.authorization.to_s
|
|
27
|
+
return if header.empty?
|
|
28
|
+
|
|
29
|
+
scheme, token = header.split(' ', 2)
|
|
30
|
+
return unless scheme.to_s.casecmp('Bearer').zero?
|
|
31
|
+
|
|
32
|
+
token.presence
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def token_from_cookies
|
|
36
|
+
return unless request.respond_to?(:cookie_jar) && request.cookie_jar
|
|
37
|
+
|
|
38
|
+
request.cookie_jar[cookie_key].presence
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/inflector'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
module SDK
|
|
7
|
+
class Client
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
@api_base_url = options.fetch(:api_base_url, Clowk.config.api_base_url)
|
|
10
|
+
@secret_key = options.fetch(:secret_key, Clowk.config.secret_key)
|
|
11
|
+
@publishable_key = options.fetch(:publishable_key, Clowk.config.publishable_key)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def method_missing(method_name, *, **, &)
|
|
15
|
+
resource_class_name = ActiveSupport::Inflector.camelize(
|
|
16
|
+
ActiveSupport::Inflector.singularize(method_name.to_s)
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return super unless Clowk::SDK.const_defined?(resource_class_name)
|
|
20
|
+
|
|
21
|
+
resource_ivar = "@#{method_name}"
|
|
22
|
+
return instance_variable_get(resource_ivar) if instance_variable_defined?(resource_ivar)
|
|
23
|
+
|
|
24
|
+
resource_class = Clowk::SDK.const_get(resource_class_name)
|
|
25
|
+
instance_variable_set(resource_ivar, resource_class.new(self))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
29
|
+
resource_class_name = ActiveSupport::Inflector.camelize(
|
|
30
|
+
ActiveSupport::Inflector.singularize(method_name.to_s)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
Clowk::SDK.const_defined?(resource_class_name) || super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete(path, body = nil, headers: {})
|
|
37
|
+
http.delete(path, body, headers:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def patch(path, body = {}, headers: {})
|
|
41
|
+
http.patch(path, body, headers:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get(path, headers: {})
|
|
45
|
+
http.get(path, headers:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def post(path, body = {}, headers: {})
|
|
49
|
+
http.post(path, body, headers:)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def put(path, body = {}, headers: {})
|
|
53
|
+
http.put(path, body, headers:)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def head(path, headers: {})
|
|
57
|
+
http.head(path, headers:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def options(path, headers: {})
|
|
61
|
+
http.options(path, headers:)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
attr_reader :api_base_url, :publishable_key, :secret_key
|
|
67
|
+
|
|
68
|
+
def http
|
|
69
|
+
@http ||= Clowk::Http.new(
|
|
70
|
+
base_url: api_base_url,
|
|
71
|
+
headers: default_headers,
|
|
72
|
+
logger: Clowk.config.http_logger,
|
|
73
|
+
open_timeout: Clowk.config.http_open_timeout,
|
|
74
|
+
read_timeout: Clowk.config.http_read_timeout,
|
|
75
|
+
write_timeout: Clowk.config.http_write_timeout,
|
|
76
|
+
retry_attempts: Clowk.config.http_retry_attempts,
|
|
77
|
+
retry_interval: Clowk.config.http_retry_interval
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def default_headers
|
|
82
|
+
{}.tap do |headers|
|
|
83
|
+
headers['X-Clowk-Secret-Key'] = secret_key if secret_key.present?
|
|
84
|
+
headers['X-Clowk-Publishable-Key'] = publishable_key if publishable_key.present?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
|
|
5
|
+
module Clowk
|
|
6
|
+
module SDK
|
|
7
|
+
class Resource
|
|
8
|
+
def self.resource_path
|
|
9
|
+
raise NotImplementedError, 'resource_path must be implemented'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(client)
|
|
13
|
+
@client = client
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Usage
|
|
17
|
+
# client.list
|
|
18
|
+
# @return [Array<Hash>] list of all resources
|
|
19
|
+
def list
|
|
20
|
+
client.get(self.class.resource_path)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @example keywords
|
|
24
|
+
# client.users.search(email: "user@example.com", status: "active")
|
|
25
|
+
# # GET /users/search?query=email%3Auser%40example.com+status%3Aactive
|
|
26
|
+
#
|
|
27
|
+
# @example raw string
|
|
28
|
+
# client.users.search("email:user@example.com active:true created_at>2026-01-01")
|
|
29
|
+
# # GET /users/search?query=email%3Auser%40example.com+active%3Atrue+created_at%3E2026-01-01
|
|
30
|
+
#
|
|
31
|
+
# @return [Clowk::Http::Response]
|
|
32
|
+
def search(raw_query = nil, **filters)
|
|
33
|
+
query = if raw_query.is_a?(String)
|
|
34
|
+
raw_query
|
|
35
|
+
else
|
|
36
|
+
filters.map { |k, v| "#{k}:#{v}" }.join(' ')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
client.get("#{self.class.resource_path}/search?query=#{ERB::Util.url_encode(query)}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Usage
|
|
43
|
+
# client.find("123")
|
|
44
|
+
# @return [Hash] resource with the given id
|
|
45
|
+
def find(id)
|
|
46
|
+
client.get("#{self.class.resource_path}/#{id}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Usage
|
|
50
|
+
# client.show("123")
|
|
51
|
+
# @return [Hash] resource with the given id
|
|
52
|
+
def show(id)
|
|
53
|
+
client.get("#{self.class.resource_path}/#{id}")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Usage
|
|
57
|
+
# client.destroy("123")
|
|
58
|
+
# @return [Hash] deleted resource
|
|
59
|
+
def destroy(id)
|
|
60
|
+
client.delete("#{self.class.resource_path}/#{id}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
attr_reader :client
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|