ecertic 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.
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module API
5
+ class DocumentsService < Service
6
+ def pdf(document_id, options = {})
7
+ client.get("/pdf/#{document_id}/0", options)
8
+ end
9
+
10
+ def html(document_id, options = {})
11
+ client.get("/html/#{document_id}/0", options)
12
+ end
13
+
14
+ def signed(document_id, options = {})
15
+ client.get("/signed/#{document_id}/0", options)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module API
5
+ class OTPsService < Service
6
+ def create(attributes, options = {})
7
+ Ecertic::Utils.validate_mandatory_attributes(attributes, [:movil, :pdf_files])
8
+ attributes[:pdf_files] = Utils.encode_files(attributes[:pdf_files])
9
+ response = client.post("/sms", attributes, options)
10
+ Resource::OTP::Request.new(response.body)
11
+ end
12
+
13
+ def status(token, options = {})
14
+ attributes = { token: token }
15
+ response = client.post("/status", attributes, options)
16
+ Resource::OTP::Status.new(response.body)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module API
5
+ class Service
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module API
5
+ class TokensService < Service
6
+ def retrieve(token, options = {})
7
+ attributes = { token: token }
8
+ response = client.post("/token", attributes, options)
9
+ Resource::Token::Instance.new(response.body)
10
+ end
11
+
12
+ def validate(token, otp, options = {})
13
+ attributes = { token: token, otp: otp }
14
+ response = client.post("/validate", attributes, options)
15
+ Resource::Token::Validation.new(response.body)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Callback
5
+ def self.parse(payload)
6
+ raise "IMPLEMENT THIS"
7
+ end
8
+ end
9
+ module Signature
10
+ def self.decrypt(payload)
11
+ raise "IMPLEMENT THIS"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ class Client
5
+ include Ecertic::API
6
+ attr_accessor :apikey, :secret, :user_agent, :connection
7
+
8
+ def initialize(options = {})
9
+ defaults = Ecertic::Default.options
10
+ Ecertic::Default.keys.each do |key|
11
+ instance_variable_set(:"@#{key}", options[key] || defaults[key])
12
+ end
13
+ @connection = connection || self.class.default_connection
14
+ @services = {}
15
+ end
16
+
17
+ def self.default_connection
18
+ Thread.current[:ecertic_client_default_connection] ||= begin
19
+ Faraday.new do |builder|
20
+ builder.use Faraday::Response::RaiseError
21
+ builder.response :json,
22
+ content_type: /\bjson$/,
23
+ preserve_raw: true, parser_options: { symbolize_names: true }
24
+ builder.adapter :net_http_persistent
25
+ end
26
+ end
27
+ end
28
+
29
+ def base_url
30
+ @base_url.chomp("/")
31
+ end
32
+
33
+ def get(path, options = {})
34
+ execute :get, path, nil, options.to_h
35
+ end
36
+
37
+ def post(path, data = nil, options = {})
38
+ execute :post, path, data, options
39
+ end
40
+
41
+ def execute(method, path, data = nil, options = {})
42
+ request(method, path, data, options)
43
+ end
44
+
45
+ def request(method, path, data = nil, options = {})
46
+ request_options = request_options(method, options, data)
47
+ uri = "#{base_url}#{path}"
48
+
49
+ begin
50
+ request_start = Time.now
51
+ log_request(method, path, request_options[:body], request_options[:headers])
52
+ response = connection.run_request(method, uri, request_options[:body], request_options[:headers]) do |req|
53
+ # req.options.open_timeout = Ecertic.open_timeout
54
+ # req.options.timeout = Ecertic.read_timeout
55
+ end
56
+ log_response(request_start, method, path, response.status, response.body)
57
+ response
58
+ rescue StandardError => e
59
+ log_response_error(request_start, e, method, path)
60
+ case e
61
+ when Faraday::ClientError
62
+ if e.response
63
+ handle_error_response(e.response)
64
+ else
65
+ handle_network_error(e)
66
+ end
67
+ else
68
+ raise
69
+ end
70
+ end
71
+ Ecertic::Response.from_faraday_response(response)
72
+ end
73
+
74
+ private def handle_network_error(error)
75
+ Ecertic::Utils.log_error("Ecertic network error", error_message: error.message)
76
+
77
+ message = case error
78
+ when Faraday::ConnectionFailed
79
+ "Unexpected error communicating when trying to connect to " \
80
+ "Ecertic. You may be seeing this message because your DNS is not " \
81
+ "working. To check, try running `host http://api.otpsecure.net` from the " \
82
+ "command line."
83
+
84
+ when Faraday::SSLError
85
+ "Could not establish a secure connection to Ecertic, you " \
86
+ "may need to upgrade your OpenSSL version. To check, try running " \
87
+ "`openssl s_client -connect api.otpsecure.net:443` from the command " \
88
+ "line."
89
+
90
+ when Faraday::TimeoutError
91
+ "Could not connect to Ecertic (#{ Ecertic.api_base}). " \
92
+ "Please check your internet connection and try again."
93
+
94
+ else
95
+ "Unexpected error communicating with Ecertic."
96
+ end
97
+ raise APIConnectionError, message + "\n\n(Network error: #{error.message})"
98
+ end
99
+
100
+ private def handle_error_response(http_response)
101
+ begin
102
+ response = Ecertic::Response.new(http_response)
103
+ rescue JSON::ParserError
104
+ raise general_api_error(http_response[:status], http_response[:body])
105
+ end
106
+
107
+ raise specific_api_error(response)
108
+ end
109
+
110
+ private def general_api_error(status, body)
111
+ APIError.new("Invalid response object from API: #{body.inspect} " \
112
+ "(HTTP response code was #{status})",
113
+ http_status: status, http_body: body)
114
+ end
115
+
116
+ private def specific_api_error(response)
117
+ Ecertic::Utils.log_error("Ecertic API error", status: response.status)
118
+
119
+ error = case response.status
120
+ when 400, 404
121
+ InvalidRequestError
122
+ when 401
123
+ AuthenticationError
124
+ else
125
+ APIError
126
+ end
127
+ error.new(response.body, response: response)
128
+ end
129
+
130
+ private def base_options
131
+ {
132
+ headers: {
133
+ "Accept" => "application/json",
134
+ "User-Agent" => format_user_agent,
135
+ },
136
+ }
137
+ end
138
+
139
+ private def request_options(method, options = {}, data = nil)
140
+ base_options.tap do |options|
141
+ add_body!(options, data) if data
142
+ add_auth_options!(method, options, data) if data
143
+ end
144
+ end
145
+
146
+ private def add_body!(options, data)
147
+ options[:headers]["Content-Type"] = content_type(options[:headers])
148
+ options[:body] = content_data(options[:headers], data)
149
+ end
150
+
151
+ private def add_auth_options!(method, options, data)
152
+ timestamp = (Time.now.to_f * 1000).to_i.to_s
153
+ hmac = hmac(method, timestamp, options[:body], options[:headers]["Content-Type"])
154
+ options[:headers]["Date"] = timestamp
155
+ options[:headers]["Authorization"] = "Hmac #{apikey}:#{hmac}"
156
+ end
157
+
158
+ private def hmac(method, timestamp, data, content_type)
159
+ md5 = Digest::MD5.hexdigest(data)
160
+ signature = "#{method.upcase}\n#{md5}\n#{content_type}\n#{timestamp}"
161
+ OpenSSL::HMAC.hexdigest("SHA1", secret, signature)
162
+ end
163
+
164
+ private def format_user_agent
165
+ if user_agent.to_s.empty?
166
+ Ecertic::Default::USER_AGENT
167
+ else
168
+ "#{Ecertic::Default::USER_AGENT} #{user_agent}"
169
+ end
170
+ end
171
+
172
+ private def content_type(headers)
173
+ headers["Content-Type"] || "application/json"
174
+ end
175
+
176
+ private def content_data(headers, data)
177
+ headers["Content-Type"] == "application/json" ? data.to_json : data
178
+ end
179
+
180
+ private def log_request(method, path, body, headers)
181
+ Ecertic::Utils.log_info("Request to Ecertic API", method: method, path: path)
182
+ Ecertic::Utils.log_debug("Request details", body: body, headers: headers)
183
+ end
184
+
185
+ private def log_response(request_start, method, path, status, body)
186
+ Ecertic::Utils.log_info("Response from Ecertic API",
187
+ elapsed: Time.now - request_start,
188
+ method: method,
189
+ path: path,
190
+ status: status)
191
+ Ecertic::Utils.log_debug("Response details", body: body)
192
+ end
193
+
194
+ private def log_response_error(request_start, error, method, path)
195
+ Ecertic::Utils.log_error("Request error",
196
+ elapsed: Time.now - request_start,
197
+ error_message: error.message,
198
+ method: method,
199
+ path: path)
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Default
5
+ BASE_URL = "https://api.otpsecure.net"
6
+ USER_AGENT = "ecertic-ruby/#{VERSION}"
7
+
8
+ class << self
9
+ def keys
10
+ @keys ||= [ :base_url, :apikey, :secret, :user_agent ]
11
+ end
12
+
13
+ def options
14
+ Hash[keys.map { |key| [key, send(key)] }]
15
+ end
16
+
17
+ def base_url
18
+ ENV["ECERTIC_BASE_URL"] || BASE_URL
19
+ end
20
+
21
+ def apikey
22
+ ENV["ECERTIC_APIKEY"]
23
+ end
24
+
25
+ def secret
26
+ ENV["ECERTIC_SECRET"]
27
+ end
28
+
29
+ def user_agent
30
+ ENV["ECERTIC_USER_AGENT"]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ # Error is the base error from which all other more specific Ecertic errors derive.
5
+ class Error < StandardError
6
+ attr_reader :message
7
+ attr_reader :http_status
8
+ attr_reader :http_headers
9
+ attr_reader :http_body
10
+ attr_reader :response
11
+
12
+ def initialize(message = nil, http_status: nil, http_headers: nil, http_body: nil, response: nil)
13
+ @message = message
14
+ @http_status = http_status || response&.status
15
+ @http_headers = http_headers || response&.headers
16
+ @http_body = http_body || response&.body
17
+ @response = response
18
+ end
19
+
20
+ def to_s
21
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
22
+ "#{status_string}#{@message}"
23
+ end
24
+ end
25
+
26
+ # InvalidRequestError is raised when a request is initiated with invalid parameters.
27
+ class InvalidRequestError < Error
28
+ end
29
+
30
+ # AuthenticationError is raised when invalid credentials are used to connect Ecertic's servers.
31
+ class AuthenticationError < Error
32
+ end
33
+
34
+ # APIConnectionError is raised in the event that the SDK can't connect to Ecertic's servers.
35
+ class APIConnectionError < Error
36
+ end
37
+
38
+ # SignatureVerificationError is raised when the decryption for a callback fails
39
+ class SignatureVerificationError < Error
40
+ end
41
+
42
+ # APIError is a generic error that may be raised in cases where none of the ther named errors cover the problem.
43
+ class APIError < Error
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Resource
5
+ class Base
6
+ def initialize(attributes = {})
7
+ attributes.each do |key, value|
8
+ m = "#{key}=".to_sym
9
+ send(m, value) if respond_to?(m)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Resource
5
+ class Document < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Resource
5
+ class OTP < Base
6
+ attr_accessor :id
7
+ attr_accessor :customid
8
+ attr_accessor :clientToken
9
+ attr_accessor :pdf_shortUrl
10
+ attr_accessor :html_shortUrl
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Resource
5
+ module OTP
6
+ class Request < Base
7
+ attr_accessor :id
8
+ attr_accessor :customid
9
+ attr_accessor :clientOtp
10
+ attr_accessor :clientToken
11
+ attr_accessor :pdf_shortUrl
12
+ attr_accessor :html_shortUrl
13
+ alias uuid id
14
+ alias otp clientOtp
15
+ alias token clientToken
16
+ alias pdf_url pdf_shortUrl
17
+ alias html_url html_shortUrl
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecertic
4
+ module Resource
5
+ module OTP
6
+ class Status < Base
7
+ include Statusable
8
+ end
9
+ end
10
+ end
11
+ end