ecertic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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