diff --git a/app/controllers/api/v1/registrant/confirms_controller.rb b/app/controllers/api/v1/registrant/confirms_controller.rb new file mode 100644 index 000000000..057400c8e --- /dev/null +++ b/app/controllers/api/v1/registrant/confirms_controller.rb @@ -0,0 +1,119 @@ +require 'serializers/registrant_api/domain' + +module Api + module V1 + module Registrant + class ConfirmsController < ::Api::V1::Registrant::BaseController + skip_before_action :authenticate, :set_paper_trail_whodunnit + before_action :set_domain, only: %i[index update] + before_action :verify_action, only: %i[index update] + before_action :verify_decision, only: %i[update] + + def index + res = { + domain_name: @domain.name, + current_registrant: serialized_registrant(@domain.registrant), + } + + unless delete_action? + res[:new_registrant] = serialized_registrant(@domain.pending_registrant) + end + + render json: res, status: :ok + end + + def update + verification = RegistrantVerification.new(domain_id: @domain.id, + verification_token: verify_params[:token]) + + unless delete_action? ? delete_action(verification) : change_action(verification) + head :bad_request + return + end + + render json: { domain_name: @domain.name, + current_registrant: serialized_registrant(current_registrant), + status: params[:decision] }, status: :ok + end + + private + + def initiator + "email link, #{I18n.t(:user_not_authenticated)}" + end + + def current_registrant + confirmed? && !delete_action? ? @domain.pending_registrant : @domain.registrant + end + + def confirmed? + verify_params[:decision] == 'confirmed' + end + + def change_action(verification) + if confirmed? + verification.domain_registrant_change_confirm!(initiator) + else + verification.domain_registrant_change_reject!(initiator) + end + end + + def delete_action(verification) + if confirmed? + verification.domain_registrant_delete_confirm!(initiator) + else + verification.domain_registrant_delete_reject!(initiator) + end + end + + def serialized_registrant(registrant) + { + name: registrant.try(:name), + ident: registrant.try(:ident), + country: registrant.try(:ident_country_code), + } + end + + def verify_params + params do |p| + p.require(:name) + p.require(:token) + p.permit(:decision) + end + end + + def delete_action? + return true if params[:template] == 'delete' + + false + end + + def verify_decision + return if %w[confirmed rejected].include?(params[:decision]) + + head :not_found + end + + def set_domain + @domain = Domain.find_by(name: verify_params[:name]) + @domain ||= Domain.find_by(name_puny: verify_params[:name]) + return if @domain + + render json: { error: 'Domain not found' }, status: :not_found + end + + def verify_action + action = if params[:template] == 'change' + @domain.registrant_update_confirmable?(verify_params[:token]) + elsif params[:template] == 'delete' + @domain.registrant_delete_confirmable?(verify_params[:token]) + end + + return if action + + render json: { error: 'Application expired or not found' }, status: :unauthorized + end + end + end + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 9269a1102..af5d59d63 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,15 @@ class ApplicationMailer < ActionMailer::Base append_view_path Rails.root.join('app', 'views', 'mailers') layout 'mailer' -end \ No newline at end of file + + def registrant_confirm_url(domain:, method:) + token = domain.registrant_verification_token + base_url = ENV['registrant_portal_verifications_base_url'] + + url = registrant_domain_delete_confirm_url(domain, token: token) if method == 'delete' + url ||= registrant_domain_update_confirm_url(domain, token: token) + return url if base_url.blank? + + "#{base_url}/confirmation/#{domain.name_puny}/#{method}/#{token}" + end +end diff --git a/app/mailers/domain_delete_mailer.rb b/app/mailers/domain_delete_mailer.rb index dbacab68d..3e4507194 100644 --- a/app/mailers/domain_delete_mailer.rb +++ b/app/mailers/domain_delete_mailer.rb @@ -2,7 +2,7 @@ class DomainDeleteMailer < ApplicationMailer def confirmation_request(domain:, registrar:, registrant:) @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) - @confirmation_url = confirmation_url(domain) + @confirmation_url = registrant_confirm_url(domain: domain, method: 'delete') subject = default_i18n_subject(domain_name: domain.name) mail(to: registrant.email, subject: subject) @@ -48,10 +48,6 @@ class DomainDeleteMailer < ApplicationMailer private - def confirmation_url(domain) - registrant_domain_delete_confirm_url(domain, token: domain.registrant_verification_token) - end - def forced_email_from ENV['action_mailer_force_delete_from'] || self.class.default[:from] end diff --git a/app/mailers/registrant_change_mailer.rb b/app/mailers/registrant_change_mailer.rb index ff3cfa18e..8f43f4ab5 100644 --- a/app/mailers/registrant_change_mailer.rb +++ b/app/mailers/registrant_change_mailer.rb @@ -5,7 +5,7 @@ class RegistrantChangeMailer < ApplicationMailer @domain = DomainPresenter.new(domain: domain, view: view_context) @registrar = RegistrarPresenter.new(registrar: registrar, view: view_context) @new_registrant = RegistrantPresenter.new(registrant: new_registrant, view: view_context) - @confirmation_url = confirmation_url(domain) + @confirmation_url = registrant_confirm_url(domain: domain, method: 'change') subject = default_i18n_subject(domain_name: domain.name) mail(to: current_registrant.email, subject: subject) @@ -49,10 +49,6 @@ class RegistrantChangeMailer < ApplicationMailer private - def confirmation_url(domain) - registrant_domain_update_confirm_url(domain, token: domain.registrant_verification_token) - end - def address_processing Contact.address_processing? end diff --git a/app/services/registrant_change.rb b/app/services/registrant_change.rb index 35b631fb6..fdee7654a 100644 --- a/app/services/registrant_change.rb +++ b/app/services/registrant_change.rb @@ -5,6 +5,7 @@ class RegistrantChange end def confirm + Dispute.close_by_domain(@domain.name) if @domain.disputed? notify_registrant end diff --git a/config/application.yml.sample b/config/application.yml.sample index e979e772a..82b45feb9 100644 --- a/config/application.yml.sample +++ b/config/application.yml.sample @@ -87,6 +87,9 @@ sk_digi_doc_service_name: 'Testimine' registrant_api_base_url: registrant_api_auth_allowed_ips: '127.0.0.1, 0.0.0.0' #ips, separated with commas +# Base URL (inc. https://) of REST registrant portal +# Leave blank to use internal registrant portal +registrant_portal_verifications_base_url: '' # # MISC diff --git a/config/routes.rb b/config/routes.rb index f58063fae..440c9c05e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -56,6 +56,8 @@ Rails.application.routes.draw do namespace :v1 do namespace :registrant do post 'auth/eid', to: 'auth#eid' + get 'confirms/:name/:template/:token', to: 'confirms#index', constraints: { name: /[^\/]+/ } + post 'confirms/:name/:template/:token/:decision', to: 'confirms#update', constraints: { name: /[^\/]+/ } resources :domains, only: %i[index show], param: :uuid do resource :registry_lock, only: %i[create destroy] diff --git a/test/integration/api/registrant/registrant_api_verifications_test.rb b/test/integration/api/registrant/registrant_api_verifications_test.rb new file mode 100644 index 000000000..821d0dee0 --- /dev/null +++ b/test/integration/api/registrant/registrant_api_verifications_test.rb @@ -0,0 +1,297 @@ +require 'test_helper' +require 'auth_token/auth_token_creator' + +class RegistrantApiVerificationsTest < ApplicationIntegrationTest + def setup + super + + @domain = domains(:hospital) + @registrant = @domain.registrant + @new_registrant = contacts(:jack) + @user = users(:api_bestnames) + + @token = 'verysecrettoken' + + @domain.update!(statuses: [DomainStatus::PENDING_UPDATE], + registrant_verification_asked_at: Time.zone.now - 1.day, + registrant_verification_token: @token) + + end + + def test_fetches_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: "hospital.test", + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + new_registrant: { + name: @new_registrant.name, + ident: @new_registrant.ident, + country: @new_registrant.ident_country_code + } + } + + assert_equal expected_body, res + end + + def test_approves_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update!(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + perform_enqueued_jobs do + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/confirmed" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @new_registrant.name, + ident: @new_registrant.ident, + country: @new_registrant.ident_country_code + }, + status: 'confirmed' + } + assert_equal expected_body, res + end + end + + def test_rejects_registrant_change_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + assert @domain.registrant_update_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/rejected" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'rejected' + } + + assert_equal expected_body, res + end + + def test_registrant_change_requires_valid_attributes + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json) + @domain.reload + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/change/123" + assert_equal 401, response.status + + get "/api/v1/registrant/confirms/aohldfjg.ee/change/123" + assert_equal 404, response.status + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/change/#{@token}/invalidaction" + assert_equal 404, response.status + end + + def test_fetches_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: "hospital.test", + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + } + } + + assert_equal expected_body, res + end + + def test_approves_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/confirmed" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'confirmed' + } + + assert_equal expected_body, res + end + + def test_rejects_domain_delete_request + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + assert @domain.registrant_delete_confirmable?(@token) + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/rejected" + assert_equal(200, response.status) + + res = JSON.parse(response.body, symbolize_names: true) + expected_body = { + domain_name: @domain.name, + current_registrant: { + name: @registrant.name, + ident: @registrant.ident, + country: @registrant.ident_country_code + }, + status: 'rejected' + } + + assert_equal expected_body, res + end + + def test_domain_delete_requires_valid_attributes + pending_json = { new_registrant_id: @new_registrant.id, + new_registrant_name: @new_registrant.name, + new_registrant_email: @new_registrant.email, + current_user_id: @user.id } + + @domain.update(pending_json: pending_json, statuses: [DomainStatus::PENDING_DELETE_CONFIRMATION]) + @domain.reload + + get "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/123" + assert_equal 401, response.status + + get "/api/v1/registrant/confirms/aohldfjg.ee/delete/123" + assert_equal 404, response.status + + post "/api/v1/registrant/confirms/#{@domain.name_puny}/delete/#{@token}/invalidaction" + assert_equal 404, response.status + end + #def test_get_non_existent_domain_details_by_uuid + # get '/api/v1/registrant/domains/random-uuid', headers: @auth_headers + # assert_equal(404, response.status) + + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [base: ['Domain not found']] }, response_json) + #end + + #def test_root_returns_domain_list + # get '/api/v1/registrant/domains', headers: @auth_headers + # assert_equal(200, response.status) + + # response_json = JSON.parse(response.body, symbolize_names: true) + # array_of_domain_names = response_json.map { |x| x[:name] } + # assert(array_of_domain_names.include?('hospital.test')) + + # array_of_domain_registrars = response_json.map { |x| x[:registrar] } + # assert(array_of_domain_registrars.include?({name: 'Good Names', website: nil})) + #end + + #def test_root_accepts_limit_and_offset_parameters + # get '/api/v1/registrant/domains', params: { 'limit' => 2, 'offset' => 0 }, + # headers: @auth_headers + # response_json = JSON.parse(response.body, symbolize_names: true) + + # assert_equal(200, response.status) + # assert_equal(2, response_json.count) + + # get '/api/v1/registrant/domains', headers: @auth_headers + # response_json = JSON.parse(response.body, symbolize_names: true) + + # assert_equal(4, response_json.count) + #end + + #def test_root_does_not_accept_limit_higher_than_200 + # get '/api/v1/registrant/domains', params: { 'limit' => 400, 'offset' => 0 }, + # headers: @auth_headers + + # assert_equal(400, response.status) + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [{ limit: ['parameter is out of range'] }] }, response_json) + #end + + #def test_root_does_not_accept_offset_lower_than_0 + # get '/api/v1/registrant/domains', params: { 'limit' => 200, 'offset' => "-10" }, + # headers: @auth_headers + + # assert_equal(400, response.status) + # response_json = JSON.parse(response.body, symbolize_names: true) + # assert_equal({ errors: [{ offset: ['parameter is out of range'] }] }, response_json) + #end + + #def test_root_returns_401_without_authorization + # get '/api/v1/registrant/domains' + # assert_equal(401, response.status) + # json_body = JSON.parse(response.body, symbolize_names: true) + + # assert_equal({ errors: [base: ['Not authorized']] }, json_body) + #end + + #def test_details_returns_401_without_authorization + # get '/api/v1/registrant/domains/5edda1a5-3548-41ee-8b65-6d60daf85a37' + # assert_equal(401, response.status) + # json_body = JSON.parse(response.body, symbolize_names: true) + + # assert_equal({ errors: [base: ['Not authorized']] }, json_body) + #end +end