diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb
index 346cd03c1..219ec1375 100644
--- a/app/controllers/eeid/webhooks/identification_requests_controller.rb
+++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb
@@ -16,7 +16,10 @@ module Eeid
return render_unauthorized unless ip_whitelisted?
return render_invalid_signature unless valid_hmac_signature?(request.headers['X-HMAC-Signature'])
- verify_contact(permitted_params[:reference])
+ contact = Contact.find_by_code(permitted_params[:reference])
+ poi = catch_poi
+ verify_contact(contact)
+ inform_registrar(contact, poi)
render json: { status: 'success' }, status: :ok
rescue StandardError => e
handle_error(e)
@@ -42,17 +45,32 @@ module Eeid
ActiveSupport::SecurityUtils.secure_compare(computed_signature, hmac_signature)
end
- def verify_contact(ref)
- contact = Contact.find_by_code(ref)
-
+ def verify_contact(contact)
+ ref = permitted_params[:reference]
if contact&.ident_request_sent_at.present?
- contact.update(verified_at: Time.zone.now)
+ contact.update(verified_at: Time.zone.now, verification_id: permitted_params[:identification_request_id])
Rails.logger.info("Contact verified: #{ref}")
else
Rails.logger.error("Valid contact not found for reference: #{ref}")
end
end
+ def catch_poi
+ ident_service = Eeid::IdentificationService.new
+ response = ident_service.get_proof_of_identity(permitted_params[:identification_request_id])
+ raise StandardError, response[:error] if response[:error].present?
+
+ response[:data]
+ end
+
+ def inform_registrar(contact, poi)
+ email = contact&.registrar&.email
+ return unless email
+
+ RegistrarMailer.contact_verified(email: email, contact: contact, poi: poi)
+ .deliver_now
+ end
+
def ip_whitelisted?
allowed_ips = ENV['webhook_allowed_ips'].to_s.split(',').map(&:strip)
@@ -67,7 +85,7 @@ module Eeid
def handle_error(error)
Rails.logger.error("Error handling webhook: #{error.message}")
- render json: { error: 'Internal Server Error' }, status: :internal_server_error
+ render json: { error: error.message }, status: :internal_server_error
end
def handle_throttle_error
diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb
index 623722cb8..7b60d8b23 100644
--- a/app/controllers/repp/v1/contacts_controller.rb
+++ b/app/controllers/repp/v1/contacts_controller.rb
@@ -2,10 +2,10 @@ require 'serializers/repp/contact'
module Repp
module V1
class ContactsController < BaseController # rubocop:disable Metrics/ClassLength
- before_action :find_contact, only: %i[show update destroy verify]
- skip_around_action :log_request, only: :search
+ before_action :find_contact, only: %i[show update destroy verify download_poi]
+ skip_around_action :log_request, only: %i[search]
- THROTTLED_ACTIONS = %i[index check search create show update destroy verify].freeze
+ THROTTLED_ACTIONS = %i[index check search create show update destroy verify download_poi].freeze
include Shunter::Integration::Throttle
api :get, '/repp/v1/contacts'
@@ -132,6 +132,19 @@ module Repp
render_success(data: data)
end
+ api :get, '/repp/v1/contacts/download_poi/:contact_code'
+ desc 'Get proof of identity pdf file for a contact'
+ def download_poi
+ authorize! :verify, Epp::Contact
+ ident_service = Eeid::IdentificationService.new
+ response = ident_service.get_proof_of_identity(@contact.verification_id)
+
+ send_data response[:data], filename: "proof_of_identity_#{@contact.verification_id}.pdf",
+ type: 'application/pdf', disposition: 'inline'
+ rescue Eeid::IdentError => e
+ handle_non_epp_errors(@contact, e.message)
+ end
+
private
def index_params
diff --git a/app/interactions/actions/contact_verify.rb b/app/interactions/actions/contact_verify.rb
index d0fedb3ec..8b9fd4fd0 100644
--- a/app/interactions/actions/contact_verify.rb
+++ b/app/interactions/actions/contact_verify.rb
@@ -23,8 +23,8 @@ module Actions
def create_identification_request
ident_service = Eeid::IdentificationService.new
- request = ident_service.create_identification_request(request_payload)
- ContactMailer.identification_requested(contact: contact, link: request['link']).deliver_now
+ response = ident_service.create_identification_request(request_payload)
+ ContactMailer.identification_requested(contact: contact, link: response['link']).deliver_now
rescue Eeid::IdentError => e
Rails.logger.error e.message
contact.errors.add(:base, :verification_error)
diff --git a/app/mailers/registrar_mailer.rb b/app/mailers/registrar_mailer.rb
new file mode 100644
index 000000000..aafc1e8ff
--- /dev/null
+++ b/app/mailers/registrar_mailer.rb
@@ -0,0 +1,10 @@
+class RegistrarMailer < ApplicationMailer
+ helper ApplicationHelper
+
+ def contact_verified(email:, contact:, poi:)
+ @contact = contact
+ subject = 'Successful Contact Verification'
+ attachments['proof_of_identity.pdf'] = poi
+ mail(to: email, subject: subject)
+ end
+end
diff --git a/app/services/eeid/base.rb b/app/services/eeid/base.rb
index 78b6716f8..2bf552221 100644
--- a/app/services/eeid/base.rb
+++ b/app/services/eeid/base.rb
@@ -85,10 +85,17 @@ module Eeid
end
def handle_response(response)
- parsed_response = JSON.parse(response.body)
+ case response['content-type']
+ when 'application/pdf', 'application/octet-stream'
+ parsed_response = { data: response.body, message: response['content-disposition'] }
+ when %r{application/json}
+ parsed_response = JSON.parse(response.body).with_indifferent_access
+ else
+ raise IdentError, 'Unsupported content type'
+ end
+
raise IdentError, parsed_response['error'] unless response.is_a?(Net::HTTPSuccess)
- Rails.logger.debug("Request successful: #{response.body}")
parsed_response
end
diff --git a/app/services/eeid/identification_service.rb b/app/services/eeid/identification_service.rb
index a3ebc321b..5757b307c 100644
--- a/app/services/eeid/identification_service.rb
+++ b/app/services/eeid/identification_service.rb
@@ -17,5 +17,9 @@ module Eeid
def get_identification_request(id)
request_endpoint("/api/ident/v1/identification_requests/#{id}")
end
+
+ def get_proof_of_identity(id)
+ request_endpoint("/api/ident/v1/identification_requests/#{id}/proof_of_identity")
+ end
end
end
diff --git a/app/views/mailers/registrar_mailer/contact_verified.html.erb b/app/views/mailers/registrar_mailer/contact_verified.html.erb
new file mode 100644
index 000000000..5a22df771
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/contact_verified.html.erb
@@ -0,0 +1,44 @@
+Tere,
+
+
+Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud. +
++Täielikud tulemused leiate manuses olevast PDF-failist. +
+Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust. + ++Parimate soovidega, +
+<%= render 'mailers/shared/signatures/signature.et.html' %> ++We are writing to inform you that the verification process for the following contact has been successfully completed. +
++The full result can be found in the attached PDF file. +
++If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us. +
++Best regards, +
+<%= render 'mailers/shared/signatures/signature.en.html' %> \ No newline at end of file diff --git a/app/views/mailers/registrar_mailer/contact_verified.text.erb b/app/views/mailers/registrar_mailer/contact_verified.text.erb new file mode 100644 index 000000000..17cb6e323 --- /dev/null +++ b/app/views/mailers/registrar_mailer/contact_verified.text.erb @@ -0,0 +1,32 @@ +Tere, + +Anname teada, et järgmise kontakti kinnitamisprotsess on edukalt lõpule viidud. + +Kontaktandmed: +- Kood: <%= @contact.code %> +- Nimi: <%= @contact.name %> +- Identifikaator: <%= ident_for(@contact) %> + +Täielikke tulemusi saab vaadata kontakti lehelt registripidaja portaalis. + +Kui Teil on küsimusi seoses kontakti kinnitamise või muude teenustega registripidaja portaalis, võtke meiega ühendust. + +Parimate soovidega, +<%= render 'mailers/shared/signatures/signature.et.text' %> +--- + +Hi, + +We are writing to inform you that the verification process for the following contact has been successfully completed. + +Contact Details: +- Code: <%= @contact.code %> +- Name: <%= @contact.name %> +- Ident: <%= ident_for(@contact) %> + +The full result can be viewed on the contact page in the Registrar Portal. + +If you have any questions regarding the contact verification or other services in the Registrar Portal, please do not hesitate to reach out to us. + +Best regards, +<%= render 'mailers/shared/signatures/signature.en.text' %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 2b12f0fda..3a1c538a8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,7 @@ Rails.application.routes.draw do get 'check/:id', to: 'contacts#check' get 'search(/:id)', to: 'contacts#search' post 'verify/:id', to: 'contacts#verify' + get 'download_poi/:id', to: 'contacts#download_poi' end end end diff --git a/db/migrate/20241015071505_add_verification_id_to_contacts.rb b/db/migrate/20241015071505_add_verification_id_to_contacts.rb new file mode 100644 index 000000000..7aff96abc --- /dev/null +++ b/db/migrate/20241015071505_add_verification_id_to_contacts.rb @@ -0,0 +1,5 @@ +class AddVerificationIdToContacts < ActiveRecord::Migration[6.1] + def change + add_column :contacts, :verification_id, :string + end +end diff --git a/db/structure.sql b/db/structure.sql index 82f10a207..d73bf3ebe 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -696,7 +696,8 @@ CREATE TABLE public.contacts ( checked_company_at timestamp without time zone, company_register_status character varying ident_request_sent_at timestamp without time zone, - verified_at timestamp without time zone + verified_at timestamp without time zone, + verification_id character varying ); @@ -5613,6 +5614,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20240816091049'), ('20240816092636'), ('20240903131540'), -('20240924103554'); +('20240924103554'), +('20241015071505'); diff --git a/test/integration/eeid/identification_requests_webhook_test.rb b/test/integration/eeid/identification_requests_webhook_test.rb index 26bbac6cd..625cfa3a5 100644 --- a/test/integration/eeid/identification_requests_webhook_test.rb +++ b/test/integration/eeid/identification_requests_webhook_test.rb @@ -11,6 +11,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest } @valid_hmac_signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json) + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) + pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf')) + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' }) + adapter = ENV['shunter_default_adapter'].constantize.new adapter&.clear! end @@ -22,6 +31,8 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest assert_response :ok assert_equal({ 'status' => 'success' }, JSON.parse(response.body)) assert_not_nil @contact.reload.verified_at + assert_equal @contact.verification_id, '123' + assert_notify_registrar('Successful Contact Verification') end test 'should return unauthorized for invalid HMAC signature' do @@ -29,13 +40,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest assert_response :unauthorized assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_emails 0 end test 'should return unauthorized for missing parameters' do post '/eeid/webhooks/identification_requests', params: { reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } assert_response :unauthorized - assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_equal({ 'error' => 'Invalid HMAC signature' }, JSON.parse(response.body)) + assert_emails 0 end test 'should handle internal server error gracefully' do @@ -44,10 +57,24 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } assert_response :internal_server_error - assert_equal({ 'error' => 'Internal Server Error' }, JSON.parse(response.body)) + assert_equal({ 'error' => 'Simulated error' }, JSON.parse(response.body)) + assert_emails 0 end end + test 'should handle error from ident response' do + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: :not_found, body: { error: 'Proof of identity not found' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + @contact.update!(ident_request_sent_at: Time.zone.now - 1.day) + post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature } + + assert_response :internal_server_error + assert_equal({ 'error' => 'Proof of identity not found' }, JSON.parse(response.body)) + assert_emails 0 + assert_nil @contact.reload.verified_at + end + test 'returns error response if throttled' do ENV['shunter_default_threshold'] = '1' ENV['shunter_enabled'] = 'true' @@ -60,4 +87,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest ENV['shunter_default_threshold'] = '10000' ENV['shunter_enabled'] = 'false' end + + private + + def assert_notify_registrar(subject) + assert_emails 1 + email = ActionMailer::Base.deliveries.last + assert_equal [@contact.registrar.email], email.to + assert_equal subject, email.subject + assert_equal 1, email.attachments.size + assert_equal 'proof_of_identity.pdf', email.attachments.first.filename + end end diff --git a/test/integration/repp/v1/contacts/download_poi_test.rb b/test/integration/repp/v1/contacts/download_poi_test.rb new file mode 100644 index 000000000..1d943dc5c --- /dev/null +++ b/test/integration/repp/v1/contacts/download_poi_test.rb @@ -0,0 +1,72 @@ +require 'test_helper' + +class ReppV1ContactsDownloadPoiTest < ActionDispatch::IntegrationTest + def setup + @contact = contacts(:john) + @user = users(:api_bestnames) + token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}") + token = "Basic #{token}" + + @auth_headers = { 'Authorization' => token } + + adapter = ENV['shunter_default_adapter'].constantize.new + adapter&.clear! + + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) + pdf_content = File.read(Rails.root.join('test/fixtures/files/legaldoc.pdf')) + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' }) + end + + def test_returns_error_when_not_found + get '/repp/v1/contacts/download_poi/nonexistant:code', headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :not_found + assert_equal 2303, json[:code] + assert_equal 'Object does not exist', json[:message] + end + + def test_downloads_poi_for_contact + @contact.update!(verified_at: Time.zone.now - 1.day, verification_id: '123') + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + + assert_response :ok + assert_equal 'application/pdf', response.headers['Content-Type'] + assert_equal "inline; filename=\"proof_of_identity_123.pdf\"; filename*=UTF-8''proof_of_identity_123.pdf", response.headers['Content-Disposition'] + assert_not_empty response.body + end + + def test_handles_non_epp_error + stub_request(:get, %r{api/ident/v1/identification_requests}) + .to_return( + status: :not_found, + body: { error: 'Proof of identity not found' }.to_json, + headers: { 'Content-Type' => 'application/json' } + ) + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 'Proof of identity not found', json[:message] + end + + def test_returns_error_response_if_throttled + ENV['shunter_default_threshold'] = '1' + ENV['shunter_enabled'] = 'true' + + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + get "/repp/v1/contacts/download_poi/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal json[:code], 2502 + assert response.body.include?(Shunter.default_error_message) + ENV['shunter_default_threshold'] = '10000' + ENV['shunter_enabled'] = 'false' + end +end diff --git a/test/integration/repp/v1/contacts/verify_test.rb b/test/integration/repp/v1/contacts/verify_test.rb index 3a48b4552..1d62c4b83 100644 --- a/test/integration/repp/v1/contacts/verify_test.rb +++ b/test/integration/repp/v1/contacts/verify_test.rb @@ -12,14 +12,18 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest adapter = ENV['shunter_default_adapter'].constantize.new adapter&.clear! - stub_request(:post, %r{api/auth/v1/token}).to_return(status: 200, body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {}) + stub_request(:post, %r{api/auth/v1/token}) + .to_return( + status: 200, + body: { access_token: 'token', token_type: 'Bearer', expires_in: 100 }.to_json, headers: {} + ) stub_request(:post, %r{api/ident/v1/identification_requests}) .with( body: { claims_required: [{ type: 'sub', value: "#{@contact.ident_country_code}#{@contact.ident}" }], reference: @contact.code } - ).to_return(status: 200, body: { id: '123' }.to_json, headers: {}) + ).to_return(status: 200, body: { id: '123' }.to_json, headers: { 'Content-Type' => 'application/json' }) end def test_returns_error_when_not_found @@ -43,6 +47,20 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest assert contact.present? assert contact.ident_request_sent_at assert_nil contact.verified_at + assert_notify_contact('Identification requested') + end + + def test_handles_non_epp_error + stub_request(:post, %r{api/ident/v1/identification_requests}) + .to_return(status: :unprocessable_entity, body: { error: 'error' }.to_json, headers: { 'Content-Type' => 'application/json' }) + + post "/repp/v1/contacts/verify/#{@contact.code}", headers: @auth_headers + json = JSON.parse(response.body, symbolize_names: true) + + assert_response :bad_request + assert_equal 'Sending identification request failed', json[:message] + assert_nil @contact.ident_request_sent_at + assert_emails 0 end def test_does_not_verify_already_verified_contact @@ -68,4 +86,13 @@ class ReppV1ContactsVerifyTest < ActionDispatch::IntegrationTest ENV['shunter_default_threshold'] = '10000' ENV['shunter_enabled'] = 'false' end + + private + + def assert_notify_contact(subject) + assert_emails 1 + email = ActionMailer::Base.deliveries.last + assert_equal [@contact.email], email.to + assert_equal subject, email.subject + end end