diff --git a/app/controllers/admin/api_users_controller.rb b/app/controllers/admin/api_users_controller.rb
index 8a22219c94..6ca2af4862 100644
--- a/app/controllers/admin/api_users_controller.rb
+++ b/app/controllers/admin/api_users_controller.rb
@@ -47,7 +47,7 @@ def destroy
def api_user_params
params.require(:api_user).permit(:username, :plain_text_password, :active,
- :identity_code, { roles: [] })
+ :subject, { roles: [] })
end
def registrar
diff --git a/app/controllers/concerns/error_and_log_handler.rb b/app/controllers/concerns/error_and_log_handler.rb
index 57f8c6b089..69e9af3547 100644
--- a/app/controllers/concerns/error_and_log_handler.rb
+++ b/app/controllers/concerns/error_and_log_handler.rb
@@ -26,7 +26,7 @@ def log_request
# rubocop:enable Metrics/MethodLength
def handle_record_not_found
- @response = { code: 2303, message: 'Object does not exist' }
+ @response = { code: 2303, message: I18n.t('repp.object_does_not_exist') }
render(json: @response, status: :not_found)
end
diff --git a/app/controllers/eeid/webhooks/identification_requests_controller.rb b/app/controllers/eeid/webhooks/identification_requests_controller.rb
index b358a736ec..36a30eb776 100644
--- a/app/controllers/eeid/webhooks/identification_requests_controller.rb
+++ b/app/controllers/eeid/webhooks/identification_requests_controller.rb
@@ -15,23 +15,48 @@ class IdentificationRequestsController < ActionController::Base
def create
return render_unauthorized unless ip_allowed?(request.remote_ip)
- contact = Contact.find_by_code(permitted_params[:reference])
- return render_invalid_signature unless valid_hmac_signature?(contact.ident_type, request.headers['X-HMAC-Signature'])
+ entity = resolve_entity
+ return render_not_found unless entity
- poi = catch_poi(contact)
- verify_contact(contact)
- inform_registrar(contact, poi)
- render json: { status: 'success' }, status: :ok
+ return render_invalid_signature unless valid_hmac_signature?(ident_type_for(entity), request.headers['X-HMAC-Signature'])
+
+ ::PaperTrail.request(whodunnit: webhook_whodunnit(entity)) do
+ poi = catch_poi(entity)
+ process_verification(entity)
+ inform_registrar(entity, poi)
+ render json: { status: 'success' }, status: :ok
+ end
rescue StandardError => e
handle_error(e)
end
- private
+ private
def permitted_params
params.permit(:identification_request_id, :reference, :client_id)
end
+ def resolve_entity
+ reference = permitted_params[:reference]
+ return nil if reference.blank?
+
+ Contact.find_by(code: reference) || ApiUser.find_by(uuid: reference)
+ end
+
+ def ident_type_for(entity)
+ entity.is_a?(ApiUser) ? 'priv' : entity.ident_type
+ end
+
+ def webhook_whodunnit(entity)
+ identifier = entity.is_a?(ApiUser) ? entity.username : entity.code
+ "eeid-webhook:#{entity.class.name}:#{identifier}"
+ end
+
+ def render_not_found
+ Rails.logger.error("Webhook reference not found: #{permitted_params[:reference]}")
+ render json: { error: 'Reference not found' }, status: :not_found
+ end
+
def render_unauthorized
Rails.logger.debug("IPAddress #{request.remote_ip} not authorized")
render json: { error: "IPAddress #{request.remote_ip} not authorized" }, status: :unauthorized
@@ -63,30 +88,71 @@ def valid_hmac_signature?(ident_type, hmac_signature)
result
end
- def verify_contact(contact)
- ref = permitted_params[:reference]
- if contact&.ident_request_sent_at.present?
- contact.update(verified_at: Time.zone.now, verification_id: permitted_params[:identification_request_id])
- Rails.logger.info("Contact verified: #{ref}")
+ def process_verification(entity)
+ if entity.is_a?(ApiUser)
+ process_api_user(entity)
else
- Rails.logger.error("Valid contact not found for reference: #{ref}")
+ process_contact(entity)
end
end
- def catch_poi(contact)
+ def process_contact(contact)
ident_service = Eeid::IdentificationService.new(contact.ident_type)
+ response = ident_service.get_identification_request(permitted_params[:identification_request_id])
+ result = response[:result] || response['result'] || {}
+
+ @contact_outcome = Actions::ProcessContactIdentificationWebhook.new(
+ contact,
+ identification_request_id: permitted_params[:identification_request_id],
+ result: result
+ ).call
+ end
+
+ def process_api_user(api_user)
+ ident_service = Eeid::IdentificationService.new('priv')
+ response = ident_service.get_identification_request(permitted_params[:identification_request_id])
+ result = response[:result] || response['result'] || {}
+
+ @api_user_outcome = Actions::ProcessApiUserIdentificationWebhook.new(
+ api_user,
+ identification_request_id: permitted_params[:identification_request_id],
+ result: result
+ ).call
+ end
+
+ def catch_poi(entity)
+ ident_service = Eeid::IdentificationService.new(ident_type_for(entity))
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
+ def inform_registrar(entity, poi)
+ email = entity&.registrar&.email
return unless email
- RegistrarMailer.contact_verified(email: email, contact: contact, poi: poi)
- .deliver_now
+ if entity.is_a?(ApiUser)
+ inform_registrar_api_user(email, entity, poi)
+ else
+ inform_registrar_contact(email, entity, poi)
+ end
+ end
+
+ def inform_registrar_contact(email, contact, poi)
+ if contact.verification_pending_at.present?
+ RegistrarMailer.contact_verification_pending(email: email, contact: contact, poi: poi).deliver_now
+ elsif contact.verified_at.present?
+ RegistrarMailer.contact_verified(email: email, contact: contact, poi: poi).deliver_now
+ end
+ end
+
+ def inform_registrar_api_user(email, api_user, poi)
+ if api_user.verification_pending_at.present?
+ RegistrarMailer.api_user_verification_pending(email: email, api_user: api_user, poi: poi).deliver_now
+ elsif api_user.verified_at.present?
+ RegistrarMailer.api_user_verified(email: email, api_user: api_user, poi: poi).deliver_now
+ end
end
def ip_allowed?(ip)
diff --git a/app/controllers/repp/v1/api_users_controller.rb b/app/controllers/repp/v1/api_users_controller.rb
index 6598021294..ac20ccef56 100644
--- a/app/controllers/repp/v1/api_users_controller.rb
+++ b/app/controllers/repp/v1/api_users_controller.rb
@@ -2,10 +2,10 @@
module Repp
module V1
class ApiUsersController < BaseController
- before_action :find_api_user, only: %i[show update destroy]
+ before_action :find_api_user, only: %i[show update destroy verify download_poi approve_verification reject_verification]
load_and_authorize_resource
- THROTTLED_ACTIONS = %i[index show create update destroy].freeze
+ THROTTLED_ACTIONS = %i[index show create update destroy verify download_poi approve_verification reject_verification].freeze
include Shunter::Integration::Throttle
api :GET, '/repp/v1/api_users'
@@ -59,6 +59,64 @@ def destroy
render_success
end
+ api :POST, '/repp/v1/api_users/verify/:id'
+ desc 'Generate and send identification request to an api user'
+ def verify
+ authorize! :verify, ApiUser
+ action = Actions::ApiUserVerify.new(@api_user)
+
+ unless action.call
+ handle_non_epp_errors(@api_user)
+ return
+ end
+
+ render_success(data: { api_user: { id: @api_user.id } })
+ end
+
+ api :GET, '/repp/v1/api_users/download_poi/:id'
+ desc 'Get proof of identity pdf file for an api user'
+ def download_poi
+ authorize! :verify, ApiUser
+ ident_service = Eeid::IdentificationService.new('priv')
+ response = ident_service.get_proof_of_identity(@api_user.verification_id)
+
+ send_data response[:data], filename: "proof_of_identity_#{@api_user.verification_id}.pdf",
+ type: 'application/pdf', disposition: 'inline'
+ rescue Eeid::IdentError => e
+ handle_non_epp_errors(@api_user, e.message)
+ end
+
+ api :POST, '/repp/v1/api_users/approve_verification/:id'
+ desc 'Manually approve pending api user identification'
+ def approve_verification
+ authorize! :verify, ApiUser
+ action = Actions::ApiUserApproveVerification.new(
+ @api_user,
+ subject: approve_verification_params[:subject]
+ )
+
+ unless action.call
+ handle_non_epp_errors(@api_user)
+ return
+ end
+
+ render_success(data: { api_user: { id: @api_user.id } })
+ end
+
+ api :POST, '/repp/v1/api_users/reject_verification/:id'
+ desc 'Reject pending api user identification'
+ def reject_verification
+ authorize! :verify, ApiUser
+ action = Actions::ApiUserRejectVerification.new(@api_user)
+
+ unless action.call
+ handle_non_epp_errors(@api_user)
+ return
+ end
+
+ render_success(data: { api_user: { id: @api_user.id } })
+ end
+
private
def find_api_user
@@ -67,7 +125,11 @@ def find_api_user
def api_user_params
params.require(:api_user).permit(:username, :plain_text_password, :active,
- :identity_code, { roles: [] })
+ :subject, :email, :identity_code, { roles: [] })
+ end
+
+ def approve_verification_params
+ params.fetch(:api_user, {}).permit(:subject)
end
def serialized_users(users)
diff --git a/app/controllers/repp/v1/base_controller.rb b/app/controllers/repp/v1/base_controller.rb
index 7c5aaec3c9..30aea3488a 100644
--- a/app/controllers/repp/v1/base_controller.rb
+++ b/app/controllers/repp/v1/base_controller.rb
@@ -35,7 +35,8 @@ def set_paper_trail_whodunnit
end
def render_success(code: nil, message: nil, data: nil)
- @response = { code: code || 1000, message: message || 'Command completed successfully',
+ @response = { code: code || 1000,
+ message: message || I18n.t('repp.command_completed_successfully'),
data: data || {} }
render(json: @response, status: :ok)
@@ -82,17 +83,28 @@ def basic_token
def authenticate_user
username, password = Base64.urlsafe_decode64(basic_token).split(':', 2)
@current_user ||= ApiUser.find_by(username: username, plain_text_password: password)
- user_active = @current_user.active?
+ user_eligible = @current_user&.eligible_for_sign_in?
- return if @current_user && user_active
+ return if @current_user && user_eligible
raise(ArgumentError)
rescue NoMethodError, ArgumentError
- @response = { code: 2202, message: 'Invalid authorization information',
- data: { username: username, password: password, active: user_active } }
+ @response = {
+ code: 2202,
+ message: authentication_failure_message,
+ data: { username: username, password: password, eligible_for_sign_in: user_eligible }
+ }
render(json: @response, status: :unauthorized)
end
+ def authentication_failure_message
+ if @current_user&.active? && !@current_user.identity_verified?
+ I18n.t('registrar.authorization.identity_not_verified')
+ else
+ I18n.t('registrar.authorization.invalid_authorization_information')
+ end
+ end
+
def check_api_ip_restriction
return if webclient_request?
return if @current_user.registrar.api_ip_white?(request.ip)
diff --git a/app/controllers/repp/v1/contacts_controller.rb b/app/controllers/repp/v1/contacts_controller.rb
index a9b0c783ce..acf098f33a 100644
--- a/app/controllers/repp/v1/contacts_controller.rb
+++ b/app/controllers/repp/v1/contacts_controller.rb
@@ -2,10 +2,11 @@
module Repp
module V1
class ContactsController < BaseController # rubocop:disable Metrics/ClassLength
- before_action :find_contact, only: %i[show update destroy verify download_poi]
+ before_action :find_contact, only: %i[show update destroy verify download_poi approve_verification reject_verification]
skip_around_action :log_request, only: %i[search]
- THROTTLED_ACTIONS = %i[index check search create show update destroy verify download_poi].freeze
+ THROTTLED_ACTIONS = %i[index check search create show update destroy verify download_poi
+ approve_verification reject_verification].freeze
include Shunter::Integration::Throttle
api :get, '/repp/v1/contacts'
@@ -144,6 +145,34 @@ def download_poi
handle_non_epp_errors(@contact, e.message)
end
+ api :POST, '/repp/v1/contacts/approve_verification/:contact_code'
+ desc 'Manually approve pending contact identification'
+ def approve_verification
+ authorize! :verify, Epp::Contact
+ action = Actions::ContactApproveVerification.new(@contact)
+
+ unless action.call
+ handle_non_epp_errors(@contact)
+ return
+ end
+
+ render_success(data: { contact: { code: @contact.code } })
+ end
+
+ api :POST, '/repp/v1/contacts/reject_verification/:contact_code'
+ desc 'Reject pending contact identification'
+ def reject_verification
+ authorize! :verify, Epp::Contact
+ action = Actions::ContactRejectVerification.new(@contact)
+
+ unless action.call
+ handle_non_epp_errors(@contact)
+ return
+ end
+
+ render_success(data: { contact: { code: @contact.code } })
+ end
+
private
def index_params
@@ -206,7 +235,7 @@ def contact_addr_present?
def create_update_success_body
{ code: opt_addr? ? 1100 : nil,
data: { contact: { code: @contact.code } },
- message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : nil }
+ message: opt_addr? ? I18n.t('epp.contacts.completed_without_address') : I18n.t('repp.command_completed_successfully') }
end
def opt_addr?
diff --git a/app/controllers/repp/v1/domains/renews_controller.rb b/app/controllers/repp/v1/domains/renews_controller.rb
index c911301190..007c32cf9c 100644
--- a/app/controllers/repp/v1/domains/renews_controller.rb
+++ b/app/controllers/repp/v1/domains/renews_controller.rb
@@ -84,7 +84,7 @@ def bulk_renew_domains
next if domain
@epp_errors.add(:epp_errors,
- msg: "Object does not exist: #{idn}",
+ msg: I18n.t('repp.object_does_not_exist_with_name', name: idn),
code: '2304')
end
else
diff --git a/app/controllers/repp/v1/registrar/auth_controller.rb b/app/controllers/repp/v1/registrar/auth_controller.rb
index 46c1774287..258b3e5f06 100644
--- a/app/controllers/repp/v1/registrar/auth_controller.rb
+++ b/app/controllers/repp/v1/registrar/auth_controller.rb
@@ -22,11 +22,14 @@ def index
def tara_callback
user = ApiUser.from_omniauth(auth_params)
response = { code: 401, message: I18n.t(:no_such_user), data: {} }
- unless user&.active && webclient_request?
+ unless user && webclient_request?
render(json: response, status: :unauthorized)
return
end
+ ::PaperTrail.request(whodunnit: eeid_auto_verify_whodunnit(user)) do
+ auto_verify_from_eeid!(user)
+ end
token = Base64.urlsafe_encode64("#{user.username}:#{user.plain_text_password}")
data = auth_values_to_data(user, mode: 'accreditation').merge(token: token)
render_success(data: data)
@@ -34,6 +37,16 @@ def tara_callback
private
+ def eeid_auto_verify_whodunnit(user)
+ "eeid-auto-verify:#{user.username}"
+ end
+
+ def auto_verify_from_eeid!(user)
+ return if user.verified_at.present?
+
+ user.update!(verified_at: Time.zone.now, verification_pending_at: nil)
+ end
+
def auth_params
params.require(:auth).permit(:uid, :new_user_id)
end
diff --git a/app/interactions/actions/api_user_approve_verification.rb b/app/interactions/actions/api_user_approve_verification.rb
new file mode 100644
index 0000000000..a353c33e7c
--- /dev/null
+++ b/app/interactions/actions/api_user_approve_verification.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Actions
+ # Registrar manually approves a pending ApiUser identification.
+ class ApiUserApproveVerification
+ SUB_PATTERN = /\A([A-Z]{2})([0-9A-Za-z]+)\z/
+
+ attr_reader :api_user
+
+ def initialize(api_user, subject: nil)
+ @api_user = api_user
+ @subject = subject.to_s.strip.presence
+ end
+
+ def call
+ unless api_user.verification_pending_at.present?
+ api_user.errors.add(:base, :not_pending_verification)
+ return false
+ end
+
+ snapshot = (api_user.verification_snapshot || {}).with_indifferent_access
+ # Pending snapshot comes from eeID result (OIDC +sub+).
+ subject = @subject || snapshot[:sub].to_s.strip.presence
+
+ if subject.blank?
+ api_user.errors.add(:base, :missing_subject)
+ return false
+ end
+
+ if subject_conflict?(subject)
+ api_user.errors.add(:subject, :taken)
+ return false
+ end
+
+ attrs = {
+ verified_at: Time.zone.now,
+ verification_pending_at: nil,
+ subject: subject
+ }
+
+ country_code = country_code_from_subject(subject)
+ attrs[:country_code] = country_code if country_code.present?
+
+ api_user.update!(attrs)
+ true
+ end
+
+ private
+
+ def country_code_from_subject(subject)
+ match = subject.match(SUB_PATTERN)
+ match&.[](1)
+ end
+
+ def subject_conflict?(subject)
+ ApiUser.where(registrar_id: api_user.registrar_id, subject: subject)
+ .where.not(id: api_user.id)
+ .exists?
+ end
+ end
+end
diff --git a/app/interactions/actions/api_user_reject_verification.rb b/app/interactions/actions/api_user_reject_verification.rb
new file mode 100644
index 0000000000..a1e88cf23a
--- /dev/null
+++ b/app/interactions/actions/api_user_reject_verification.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Actions
+ # Clears pending ApiUser identification after registrar rejection.
+ class ApiUserRejectVerification
+ attr_reader :api_user
+
+ def initialize(api_user)
+ @api_user = api_user
+ end
+
+ def call
+ unless api_user.verification_pending_at.present?
+ api_user.errors.add(:base, :not_pending_verification)
+ return false
+ end
+
+ api_user.update!(
+ ident_request_sent_at: nil,
+ verification_pending_at: nil,
+ verification_id: nil,
+ verification_snapshot: {}
+ )
+ true
+ end
+ end
+end
diff --git a/app/interactions/actions/api_user_verify.rb b/app/interactions/actions/api_user_verify.rb
new file mode 100644
index 0000000000..cc4d6842d7
--- /dev/null
+++ b/app/interactions/actions/api_user_verify.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Actions
+ # Sends an eeID identification request for an API user.
+ class ApiUserVerify
+ attr_reader :api_user
+
+ def initialize(api_user)
+ @api_user = api_user
+ end
+
+ def call
+ return false unless validate_email!
+
+ create_identification_request
+ return false if api_user.errors.any?
+
+ commit
+ end
+
+ private
+
+ def validate_email!
+ if api_user.email.blank?
+ api_user.errors.add(:email, :blank)
+ return false
+ end
+
+ true
+ end
+
+ def create_identification_request
+ ident_service = Eeid::IdentificationService.new('priv')
+ response = ident_service.create_identification_request(request_payload)
+ ApiUserMailer.identification_requested(api_user: api_user, link: response['link']).deliver_now
+ rescue Eeid::IdentError => e
+ Rails.logger.error e.message
+ api_user.errors.add(:base, :verification_error)
+ end
+
+ def request_payload
+ {
+ claims_required: claims_required,
+ reference: api_user.uuid
+ }
+ end
+
+ def claims_required
+ if api_user.subject.present?
+ [{ type: 'sub', value: api_user.subject }]
+ else
+ [{ type: 'sub', value: '' }]
+ end
+ end
+
+ def commit
+ api_user.update(
+ ident_request_sent_at: Time.zone.now,
+ verified_at: nil,
+ verification_id: nil,
+ verification_pending_at: nil,
+ verification_snapshot: {}
+ )
+ end
+ end
+end
diff --git a/app/interactions/actions/contact_approve_verification.rb b/app/interactions/actions/contact_approve_verification.rb
new file mode 100644
index 0000000000..3c2c1eb274
--- /dev/null
+++ b/app/interactions/actions/contact_approve_verification.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Actions
+ # Registrar manually approves a pending Contact identification.
+ class ContactApproveVerification
+ SUB_PATTERN = /\A([A-Z]{2})([0-9A-Za-z]+)\z/
+
+ attr_reader :contact
+
+ def initialize(contact)
+ @contact = contact
+ end
+
+ def call
+ unless contact.verification_pending_at.present?
+ contact.errors.add(:base, :not_pending_verification)
+ return false
+ end
+
+ snapshot = (contact.verification_snapshot || {}).with_indifferent_access
+ attrs = {
+ verified_at: Time.zone.now,
+ verification_pending_at: nil,
+ verification_snapshot: {}
+ }
+
+ if contact.ident_type == Contact::BIRTHDAY
+ ident_attrs = birthday_attrs_from_snapshot(snapshot)
+ return false if ident_attrs.nil?
+
+ attrs.merge!(ident_attrs)
+ else
+ subject = snapshot[:sub].to_s.strip.presence
+ if subject.blank?
+ contact.errors.add(:base, :missing_subject)
+ return false
+ end
+
+ country_code, ident = parse_subject(subject)
+ if country_code.blank? || ident.blank?
+ contact.errors.add(:base, :missing_subject)
+ return false
+ end
+
+ attrs[:ident_country_code] = country_code
+ attrs[:ident] = ident
+ end
+
+ contact.update!(attrs)
+ true
+ end
+
+ private
+
+ def birthday_attrs_from_snapshot(snapshot)
+ if snapshot[:id_number].to_s.strip.present?
+ contact.errors.add(:base, :id_number_requires_priv_contact)
+ return nil
+ end
+
+ birthdate = snapshot[:birthdate].presence || snapshot[:date_of_birth].presence
+ name = snapshot[:name].presence ||
+ [snapshot[:given_name], snapshot[:family_name]].compact.join(' ').strip.presence
+ country_code = snapshot[:country].to_s.strip.upcase.presence
+
+ if birthdate.blank? || name.blank? || country_code.blank?
+ contact.errors.add(:base, :missing_claims)
+ return nil
+ end
+
+ {
+ ident: birthdate,
+ name: name,
+ ident_country_code: country_code
+ }
+ end
+
+ def parse_subject(subject)
+ match = subject.match(SUB_PATTERN)
+ return [nil, nil] unless match
+
+ [match[1], match[2]]
+ end
+ end
+end
diff --git a/app/interactions/actions/contact_create.rb b/app/interactions/actions/contact_create.rb
index ca1b4d7c5d..782efdf9a4 100644
--- a/app/interactions/actions/contact_create.rb
+++ b/app/interactions/actions/contact_create.rb
@@ -27,8 +27,12 @@ def maybe_change_email
@result = Actions::SimpleMailValidator.run(email: contact.email, level: m)
next if @result
- err_text = "email '#{contact.email}' didn't pass validation"
- contact.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}")
+ contact.add_epp_error(
+ '2005',
+ nil,
+ nil,
+ "#{I18n.t(:parameter_value_syntax_error)} #{I18n.t(:email_did_not_pass_validation, email: contact.email)}"
+ )
@error = true
return
end
@@ -62,10 +66,10 @@ def validate_ident_integrity
if ident[:ident_type].blank?
contact.add_epp_error('2003', nil, 'ident_type',
- I18n.t('errors.messages.required_ident_attribute_missing'))
+ I18n.t('errors.messages.required_ident_attribute_missing', key: 'ident_type'))
@error = true
elsif !%w[priv org birthday].include?(ident[:ident_type])
- contact.add_epp_error('2003', nil, 'ident_type', 'Invalid ident type')
+ contact.add_epp_error('2003', nil, 'ident_type', I18n.t('errors.messages.invalid_ident_type'))
@error = true
end
end
@@ -75,7 +79,7 @@ def validate_ident_birthday
return unless ident[:ident_type] != 'birthday' && ident[:ident_country_code].blank?
contact.add_epp_error('2003', nil, 'ident_country_code',
- I18n.t('errors.messages.required_ident_attribute_missing'))
+ I18n.t('errors.messages.required_ident_attribute_missing', key: 'ident_country_code'))
@error = true
end
diff --git a/app/interactions/actions/contact_reject_verification.rb b/app/interactions/actions/contact_reject_verification.rb
new file mode 100644
index 0000000000..cb1da94224
--- /dev/null
+++ b/app/interactions/actions/contact_reject_verification.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Actions
+ # Clears pending Contact identification after registrar rejection.
+ class ContactRejectVerification
+ attr_reader :contact
+
+ def initialize(contact)
+ @contact = contact
+ end
+
+ def call
+ unless contact.verification_pending_at.present?
+ contact.errors.add(:base, :not_pending_verification)
+ return false
+ end
+
+ contact.update!(
+ ident_request_sent_at: nil,
+ verification_pending_at: nil,
+ verification_id: nil,
+ verification_snapshot: {}
+ )
+ true
+ end
+ end
+end
diff --git a/app/interactions/actions/contact_update.rb b/app/interactions/actions/contact_update.rb
index dc9ebd944a..4869b2d6ad 100644
--- a/app/interactions/actions/contact_update.rb
+++ b/app/interactions/actions/contact_update.rb
@@ -30,8 +30,12 @@ def maybe_change_email
result = Actions::SimpleMailValidator.run(email: @new_attributes[:email], level: m)
next if result
- err_text = "email '#{new_attributes[:email]}' didn't pass validation"
- contact.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}")
+ contact.add_epp_error(
+ '2005',
+ nil,
+ nil,
+ "#{I18n.t(:parameter_value_syntax_error)} #{I18n.t(:email_did_not_pass_validation, email: new_attributes[:email])}"
+ )
@error = true
return
end
diff --git a/app/interactions/actions/contact_verify.rb b/app/interactions/actions/contact_verify.rb
index e8c4babf78..9b8c9b8d74 100644
--- a/app/interactions/actions/contact_verify.rb
+++ b/app/interactions/actions/contact_verify.rb
@@ -63,7 +63,9 @@ def commit
@contact.update(
ident_request_sent_at: Time.zone.now,
verified_at: nil,
- verification_id: nil
+ verification_id: nil,
+ verification_pending_at: nil,
+ verification_snapshot: {}
)
end
end
diff --git a/app/interactions/actions/domain_create.rb b/app/interactions/actions/domain_create.rb
index 0d7dab709a..9046b88d72 100644
--- a/app/interactions/actions/domain_create.rb
+++ b/app/interactions/actions/domain_create.rb
@@ -45,10 +45,9 @@ def validate_domain_integrity
dn = DNS::DomainName.new(domain.name)
if dn.at_auction? || dn.is_deadline_is_reached?
- domain.add_epp_error('2306', nil, nil, 'Parameter value policy error: domain is at auction')
+ domain.add_epp_error('2306', nil, nil, %i[base domain_at_auction])
elsif dn.awaiting_payment?
- domain.add_epp_error('2003', nil, nil, 'Required parameter missing; reserved>pw element' \
- ' required for reserved domains')
+ domain.add_epp_error('2003', nil, nil, %i[base required_parameter_missing_reserved])
elsif dn.pending_registration?
validate_reserved_password(dn)
end
@@ -56,12 +55,10 @@ def validate_domain_integrity
def validate_reserved_password(domain_name)
if params[:reserved_pw].blank?
- domain.add_epp_error('2003', nil, nil, 'Required parameter missing; reserved>pw ' \
- 'element is required')
+ domain.add_epp_error('2003', nil, nil, %i[base reserved_pw_element_required])
else
unless domain_name.available_with_code?(params[:reserved_pw])
- domain.add_epp_error('2202', nil, nil, 'Invalid authorization information; invalid ' \
- 'reserved>pw value')
+ domain.add_epp_error('2202', nil, nil, %i[base invalid_auth_information_reserved])
end
end
end
diff --git a/app/interactions/actions/domain_delete.rb b/app/interactions/actions/domain_delete.rb
index 750f0abac7..de318384a2 100644
--- a/app/interactions/actions/domain_delete.rb
+++ b/app/interactions/actions/domain_delete.rb
@@ -27,7 +27,7 @@ def maybe_attach_legal_doc
def verify_not_discarded
return unless domain.discarded?
- domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ domain.add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation))
end
def verify?
diff --git a/app/interactions/actions/domain_transfer.rb b/app/interactions/actions/domain_transfer.rb
index 1249846037..52cf2be7a7 100644
--- a/app/interactions/actions/domain_transfer.rb
+++ b/app/interactions/actions/domain_transfer.rb
@@ -25,7 +25,7 @@ def call
def domain_exists?
return true if domain.persisted?
- domain.add_epp_error('2303', nil, nil, 'Object does not exist')
+ domain.add_epp_error('2303', nil, nil, I18n.t('repp.object_does_not_exist'))
false
end
@@ -40,27 +40,26 @@ def run_validations
def valid_transfer_code?
return true if transfer_code == domain.transfer_code
- domain.add_epp_error('2202', nil, nil, 'Invalid authorization information')
+ domain.add_epp_error('2202', nil, nil, I18n.t('repp.errors.invalid_authorization_information'))
false
end
def validate_registrar
return unless user == domain.registrar
- domain.add_epp_error('2002', nil, nil,
- I18n.t(:domain_already_belongs_to_the_querying_registrar))
+ domain.add_epp_error('2002', nil, nil, %i[base domain_already_belongs_to_the_querying_registrar])
end
def validate_eligilibty
return unless domain.non_transferable?
- domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ domain.add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation))
end
def validate_not_discarded
return unless domain.discarded?
- domain.add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
+ domain.add_epp_error('2106', nil, nil, I18n.t('repp.errors.object_not_eligible_for_transfer'))
end
def commit
diff --git a/app/interactions/actions/domain_update.rb b/app/interactions/actions/domain_update.rb
index a162106afb..59a02f3761 100644
--- a/app/interactions/actions/domain_update.rb
+++ b/app/interactions/actions/domain_update.rb
@@ -44,7 +44,7 @@ def validate_domain_integrity
return unless domain.discarded?
- domain.add_epp_error('2304', nil, nil, 'Object status prohibits operation')
+ domain.add_epp_error('2304', nil, nil, I18n.t(:object_status_prohibits_operation))
end
def assign_new_registrant
@@ -146,8 +146,12 @@ def validate_email(email)
result = Actions::SimpleMailValidator.run(email: email, level: m)
next if result
- err_text = "email #{email} didn't pass validation"
- domain.add_epp_error('2005', nil, nil, "#{I18n.t(:parameter_value_syntax_error)} #{err_text}")
+ domain.add_epp_error(
+ '2005',
+ nil,
+ nil,
+ "#{I18n.t(:parameter_value_syntax_error)} #{I18n.t(:email_did_not_pass_validation, email: email)}"
+ )
@error = true
return false
end
@@ -285,13 +289,9 @@ def validate_dispute_case
end
if params[:reserved_pw].present?
- domain.add_epp_error(
- '2202', nil, nil, 'Invalid authorization information; invalid reserved>pw value'
- )
+ domain.add_epp_error('2202', nil, nil, %i[base invalid_auth_information_disputed])
else
- domain.add_epp_error(
- '2304', nil, nil, 'Required parameter missing; reservedpw element required for dispute domains'
- )
+ domain.add_epp_error('2304', nil, nil, %i[base required_parameter_missing_disputed])
end
false
end
diff --git a/app/interactions/actions/process_api_user_identification_webhook.rb b/app/interactions/actions/process_api_user_identification_webhook.rb
new file mode 100644
index 0000000000..712762e87f
--- /dev/null
+++ b/app/interactions/actions/process_api_user_identification_webhook.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module Actions
+ # Applies eeID identification results to an ApiUser (auto-verify or pending review).
+ class ProcessApiUserIdentificationWebhook
+ SUB_PATTERN = /\A([A-Z]{2})([0-9A-Za-z]+)\z/
+
+ attr_reader :api_user, :outcome
+
+ def initialize(api_user, identification_request_id:, result:)
+ @api_user = api_user
+ @identification_request_id = identification_request_id
+ @result = (result || {}).with_indifferent_access
+ end
+
+ def call
+ unless api_user.ident_request_sent_at.present?
+ Rails.logger.error("ApiUser verification ignored: ident not requested for user #{api_user.id}")
+ @outcome = :ignored
+ return false
+ end
+
+ if auto_verifiable?
+ apply_auto_verification!
+ @outcome = :auto_verified
+ else
+ apply_pending_review!(pending_reason)
+ @outcome = :pending_review
+ end
+
+ true
+ end
+
+ def pending_reason
+ @pending_reason ||= compute_pending_reason
+ end
+
+ private
+
+ def compute_pending_reason
+ subject = result_subject
+ return :missing_subject if subject.blank?
+
+ return :subject_conflict if subject_conflict?(subject)
+ return :subject_mismatch if pre_set_subject_mismatch?(subject)
+
+ nil
+ end
+
+ def auto_verifiable?
+ pending_reason.nil?
+ end
+
+ # eeID identification result uses OIDC userinfo; login id is in +sub+, not +subject+.
+ def result_subject
+ @result[:sub].to_s.strip.presence
+ end
+
+ def subject_conflict?(subject)
+ ApiUser.where(registrar_id: api_user.registrar_id, subject: subject)
+ .where.not(id: api_user.id)
+ .exists?
+ end
+
+ def pre_set_subject_mismatch?(subject)
+ expected = api_user.subject.presence || subject_from_identity_code
+ expected.present? && expected != subject
+ end
+
+ def subject_from_identity_code
+ return nil if api_user.identity_code.blank?
+
+ country = api_user.country_code.presence || 'EE'
+ "#{country}#{api_user.identity_code}"
+ end
+
+ def apply_auto_verification!
+ subject = result_subject
+ attrs = {
+ subject: subject,
+ verified_at: Time.zone.now,
+ verification_id: @identification_request_id,
+ verification_pending_at: nil,
+ verification_snapshot: {}
+ }
+
+ country_code = country_code_from_subject(subject)
+ attrs[:country_code] = country_code if country_code.present?
+
+ api_user.update!(attrs)
+ Rails.logger.info("ApiUser verified (auto): #{api_user.id}")
+ end
+
+ def country_code_from_subject(subject)
+ match = subject.match(SUB_PATTERN)
+ match&.[](1)
+ end
+
+ def apply_pending_review!(reason)
+ api_user.update!(
+ verification_id: @identification_request_id,
+ verification_pending_at: Time.zone.now,
+ verification_snapshot: verification_snapshot,
+ verified_at: nil
+ )
+ Rails.logger.info("ApiUser verification pending (#{reason}): #{api_user.id}")
+ end
+
+ def verification_snapshot
+ @result.slice(:sub, :given_name, :family_name, :name, :date_of_birth, :birthdate, :country,
+ :authentication_type).compact
+ end
+ end
+end
diff --git a/app/interactions/actions/process_contact_identification_webhook.rb b/app/interactions/actions/process_contact_identification_webhook.rb
new file mode 100644
index 0000000000..4b810ca89f
--- /dev/null
+++ b/app/interactions/actions/process_contact_identification_webhook.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+module Actions
+ # Applies eeID identification results to a Contact (auto-verify or pending review).
+ class ProcessContactIdentificationWebhook
+ attr_reader :contact, :outcome
+
+ def initialize(contact, identification_request_id:, result:)
+ @contact = contact
+ @identification_request_id = identification_request_id
+ @result = (result || {}).with_indifferent_access
+ end
+
+ def call
+ unless contact.ident_request_sent_at.present?
+ Rails.logger.error("Contact verification ignored: ident not requested for contact #{contact.id}")
+ @outcome = :ignored
+ return false
+ end
+
+ if auto_verifiable?
+ apply_auto_verification!
+ @outcome = :auto_verified
+ else
+ apply_pending_review!(pending_reason)
+ @outcome = :pending_review
+ end
+
+ true
+ end
+
+ def pending_reason
+ @pending_reason ||= compute_pending_reason
+ end
+
+ private
+
+ def compute_pending_reason
+ if contact.ident_type == Contact::BIRTHDAY
+ return :id_number_requires_priv if id_number_from_result.present?
+ return :missing_claims if birthday_claims_incomplete?
+ return :claim_mismatch if birthday_claim_mismatch?
+ else
+ subject = result_subject
+ return :missing_subject if subject.blank?
+ return :ident_mismatch if ident_mismatch?(subject)
+ end
+
+ nil
+ end
+
+ def auto_verifiable?
+ pending_reason.nil?
+ end
+
+ def result_subject
+ @result[:sub].to_s.strip.presence
+ end
+
+ def expected_subject
+ "#{contact.ident_country_code}#{contact.ident}"
+ end
+
+ def ident_mismatch?(subject)
+ subject != expected_subject
+ end
+
+ def birthday_claims_incomplete?
+ birthdate_from_result.blank? || name_from_result.blank? || country_from_result.blank?
+ end
+
+ def birthday_claim_mismatch?
+ normalize(birthdate_from_result) != normalize(contact.ident) ||
+ normalize(name_from_result) != normalize(contact.name) ||
+ normalize(country_from_result) != normalize(contact.ident_country_code)
+ end
+
+ def birthdate_from_result
+ @result[:birthdate].presence || @result[:date_of_birth].presence
+ end
+
+ def name_from_result
+ @result[:name].presence ||
+ [@result[:given_name], @result[:family_name]].compact.join(' ').strip.presence
+ end
+
+ def country_from_result
+ @result[:country].to_s.strip.upcase.presence
+ end
+
+ def id_number_from_result
+ @result[:id_number].to_s.strip.presence
+ end
+
+ def normalize(value)
+ value.to_s.strip.upcase
+ end
+
+ def apply_auto_verification!
+ contact.update!(
+ verified_at: Time.zone.now,
+ verification_id: @identification_request_id,
+ verification_pending_at: nil,
+ verification_snapshot: {}
+ )
+ Rails.logger.info("Contact verified (auto): #{contact.id}")
+ end
+
+ def apply_pending_review!(reason)
+ contact.update!(
+ verification_id: @identification_request_id,
+ verification_pending_at: Time.zone.now,
+ verification_snapshot: verification_snapshot,
+ verified_at: nil
+ )
+ Rails.logger.info("Contact verification pending (#{reason}): #{contact.id}")
+ end
+
+ def verification_snapshot
+ @result.slice(:sub, :given_name, :family_name, :name, :date_of_birth, :birthdate, :country,
+ :authentication_type, :id_number, :document_number).compact
+ end
+ end
+end
diff --git a/app/lib/api_users/subject_backfill.rb b/app/lib/api_users/subject_backfill.rb
new file mode 100644
index 0000000000..831b97b1f3
--- /dev/null
+++ b/app/lib/api_users/subject_backfill.rb
@@ -0,0 +1,33 @@
+module ApiUsers
+ module SubjectBackfill
+ module_function
+
+ def run
+ updated = 0
+ skipped = 0
+
+ ApiUser.where(subject: [nil, ''])
+ .where.not(identity_code: [nil, ''])
+ .find_each do |user|
+ country = user.country_code.presence || 'EE'
+ subject = "#{country}#{user.identity_code}"
+
+ if ApiUser.where(registrar_id: user.registrar_id, subject: subject)
+ .where.not(id: user.id)
+ .exists?
+ skipped += 1
+ Rails.logger.warn(
+ "api_user subject backfill skipped user_id=#{user.id} " \
+ "registrar_id=#{user.registrar_id} subject=#{subject} (conflict)"
+ )
+ next
+ end
+
+ user.update_columns(subject: subject)
+ updated += 1
+ end
+
+ { updated: updated, skipped: skipped }
+ end
+ end
+end
diff --git a/app/mailers/api_user_mailer.rb b/app/mailers/api_user_mailer.rb
new file mode 100644
index 0000000000..e1d00fa2f0
--- /dev/null
+++ b/app/mailers/api_user_mailer.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class ApiUserMailer < ApplicationMailer
+ def identification_requested(api_user:, link:)
+ @api_user = api_user
+ @verification_link = link
+
+ subject = default_i18n_subject(username: api_user.username)
+ mail(to: api_user.email, subject: subject)
+ end
+end
diff --git a/app/mailers/registrar_mailer.rb b/app/mailers/registrar_mailer.rb
index 99a3645273..7e9a78e7d0 100644
--- a/app/mailers/registrar_mailer.rb
+++ b/app/mailers/registrar_mailer.rb
@@ -7,4 +7,35 @@ def contact_verified(email:, contact:, poi:)
attachments['proof_of_identity.pdf'] = poi
mail(to: email, subject: subject)
end
+
+ def contact_verification_pending(email:, contact:, poi:)
+ @contact = contact
+ @verification_snapshot = contact.verification_snapshot
+ subject = default_i18n_subject(contact_code: contact.code)
+ attachments['proof_of_identity.pdf'] = poi if poi.present?
+ mail(to: email, subject: subject)
+ end
+
+ def api_user_verified(email:, api_user:, poi:)
+ @api_user = api_user
+ subject = default_i18n_subject(username: api_user.username)
+ attachments['proof_of_identity.pdf'] = poi
+ mail(to: email, subject: subject)
+ end
+
+ def api_user_verification_pending(email:, api_user:, poi:)
+ @api_user = api_user
+ @verification_snapshot = api_user.verification_snapshot
+ subject = default_i18n_subject(username: api_user.username)
+ attachments['proof_of_identity.pdf'] = poi if poi.present?
+ mail(to: email, subject: subject)
+ end
+
+ def api_user_subject_changed(email:, api_user:, old_subject:, new_subject:)
+ @api_user = api_user
+ @old_subject = old_subject
+ @new_subject = new_subject
+ subject = default_i18n_subject(username: api_user.username)
+ mail(to: email, subject: subject)
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 6e5071de79..26e6668ff1 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -30,6 +30,7 @@ def super # Registrar/api_user dynamic role
epp
billing
can :manage, ApiUser
+ can :verify, ApiUser
can :manage, WhiteIp
can :manage, Certificate
end
diff --git a/app/models/api_user.rb b/app/models/api_user.rb
index b06ba29644..28561c72ae 100644
--- a/app/models/api_user.rb
+++ b/app/models/api_user.rb
@@ -8,7 +8,13 @@ class ApiUser < User
def epp_code_map
{
'2306' => [ # Parameter policy error
- %i[plain_text_password blank]
+ %i[plain_text_password blank],
+ %i[email blank],
+ %i[base verification_error],
+ %i[base not_pending_verification],
+ %i[identity_code taken],
+ %i[subject taken],
+ %i[base missing_subject]
]
}
end
@@ -21,10 +27,32 @@ def self.min_password_length # Must precede .validates
belongs_to :registrar
has_many :certificates
+ VALID_EMAIL_REGEX = /\A([\w+\-]\.?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i.freeze
+
validates :username, :plain_text_password, :registrar, :roles, presence: true
validates :plain_text_password, length: { minimum: min_password_length }
validates :username, uniqueness: true
+ validates :email, format: { with: VALID_EMAIL_REGEX, allow_blank: true }
validates :identity_code, uniqueness: { scope: :registrar_id }, if: -> { identity_code.present? }
+ validates :subject, uniqueness: { scope: :registrar_id }, if: -> { subject.present? }
+ before_validation :clear_verification_status_on_subject_change, if: :subject_changed_from_existing?
+ after_commit :notify_registrar_subject_changed, on: :update
+
+ scope :eligible_for_sign_in, lambda {
+ where(active: true).where.not(verified_at: nil)
+ }
+
+ def identity_verified?
+ verified_at.present?
+ end
+
+ def eligible_for_sign_in?
+ active? && identity_verified?
+ end
+
+ def verification_pending?
+ verification_pending_at.present?
+ end
delegate :code, :name, to: :registrar, prefix: true
delegate :legaldoc_mandatory?, to: :registrar
@@ -74,8 +102,10 @@ def pki_ok?(crt, com, api: true)
end
def linked_users
- self.class.where(identity_code: identity_code, active: true)
- .where("identity_code IS NOT NULL AND identity_code != ''")
+ return self.class.none if subject.blank?
+
+ self.class.where(subject: subject, active: true)
+ .where.not(verified_at: nil)
.where.not(id: id)
.includes(:registrar)
end
@@ -85,7 +115,9 @@ def api_users
end
def linked_with?(another_api_user)
- another_api_user.identity_code == identity_code
+ return false if another_api_user.blank? || subject.blank?
+
+ another_api_user.subject == subject
end
def as_csv_row
@@ -114,6 +146,43 @@ def self.ransackable_attributes(*)
private
+ def subject_changed_from_existing?
+ return false unless persisted?
+ return false unless will_save_change_to_subject?
+ return false if subject_in_database.to_s.blank?
+
+ subject.to_s != subject_in_database.to_s
+ end
+
+ def clear_verification_status_on_subject_change
+ @subject_change_notification_data = {
+ old_subject: subject_in_database.to_s,
+ new_subject: subject.to_s
+ }
+
+ self.ident_request_sent_at = nil
+ self.verified_at = nil
+ self.verification_id = nil
+ self.verification_pending_at = nil
+ self.verification_snapshot = {}
+ end
+
+ def notify_registrar_subject_changed
+ data = @subject_change_notification_data
+ @subject_change_notification_data = nil
+ return if data.blank?
+
+ email = registrar&.email
+ return if email.blank?
+
+ RegistrarMailer.api_user_subject_changed(
+ email: email,
+ api_user: self,
+ old_subject: data[:old_subject],
+ new_subject: data[:new_subject]
+ ).deliver_now
+ end
+
def machine_readable_certificate(cert)
Rails.logger.debug "[machine_readable_certificate] Original cert: #{cert}"
diff --git a/app/models/epp/domain.rb b/app/models/epp/domain.rb
index d765f6bded..49e7b7a7e0 100644
--- a/app/models/epp/domain.rb
+++ b/app/models/epp/domain.rb
@@ -239,7 +239,7 @@ def add_renew_epp_errors
def transfer(frame, action, current_user)
if discarded?
- add_epp_error('2106', nil, nil, 'Object is not eligible for transfer')
+ add_epp_error('2106', nil, nil, I18n.t('repp.errors.object_not_eligible_for_transfer'))
return
end
diff --git a/app/models/invoice_state_machine.rb b/app/models/invoice_state_machine.rb
index 0474778009..ebc22e37e3 100644
--- a/app/models/invoice_state_machine.rb
+++ b/app/models/invoice_state_machine.rb
@@ -46,7 +46,7 @@ def mark_as_unpaid
end
def push_error
- invoice.errors.add(:base, "Inavalid state #{status}")
+ invoice.errors.add(:base, :invalid_state, status: status)
false
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 980c3cf44f..109b341190 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -14,10 +14,9 @@ def id_role_username
end
def self.from_omniauth(omniauth_hash)
- uid = omniauth_hash['uid']
- identity_code = uid&.slice(2..-1)
- # country_code = uid.slice(0..1)
+ uid = omniauth_hash['uid'] || omniauth_hash[:uid]
+ return if uid.blank?
- find_by(identity_code: identity_code, active: true)
+ ApiUser.find_by(subject: uid, active: true)
end
end
diff --git a/app/views/admin/api_users/_form.html.erb b/app/views/admin/api_users/_form.html.erb
index ad6ee6b2b3..d91351928e 100644
--- a/app/views/admin/api_users/_form.html.erb
+++ b/app/views/admin/api_users/_form.html.erb
@@ -23,10 +23,10 @@
diff --git a/app/views/admin/api_users/show/_details.html.erb b/app/views/admin/api_users/show/_details.html.erb
index c98f23866e..9dee079a8e 100644
--- a/app/views/admin/api_users/show/_details.html.erb
+++ b/app/views/admin/api_users/show/_details.html.erb
@@ -13,6 +13,9 @@
<%= ApiUser.human_attribute_name :plain_text_password %>
<%= @api_user.plain_text_password %>
+ <%= ApiUser.human_attribute_name :subject %>
+ <%= @api_user.subject %>
+
<%= Registrar.model_name.human %>
<%= link_to(@api_user.registrar, admin_registrar_path(@api_user.registrar)) %>
@@ -21,6 +24,18 @@
<%= ApiUser.human_attribute_name :active %>
<%= @api_user.active %>
+
+ <%= ApiUser.human_attribute_name :ident_request_sent_at %>
+ <%= @api_user.ident_request_sent_at %>
+
+ <%= ApiUser.human_attribute_name :verified_at %>
+ <%= @api_user.verified_at %>
+
+ <%= ApiUser.human_attribute_name :verification_pending_at %>
+ <%= @api_user.verification_pending_at %>
+
+ <%= ApiUser.human_attribute_name :verification_id %>
+ <%= @api_user.verification_id %>
diff --git a/app/views/admin/registrars/show/_api_users.html.erb b/app/views/admin/registrars/show/_api_users.html.erb
index ed6a0b6001..42dd3c7a9e 100644
--- a/app/views/admin/registrars/show/_api_users.html.erb
+++ b/app/views/admin/registrars/show/_api_users.html.erb
@@ -6,8 +6,9 @@
- | <%= ApiUser.human_attribute_name :username %> |
- <%= ApiUser.human_attribute_name :active %> |
+ <%= ApiUser.human_attribute_name :username %> |
+ <%= t('.verification_status') %> |
+ <%= ApiUser.human_attribute_name :active %> |
@@ -15,6 +16,17 @@
<% registrar.api_users.each do |api_user| %>
| <%= link_to api_user, admin_registrar_api_user_path(registrar, api_user) %> |
+
+ <% if api_user.verified_at.present? %>
+ <%= t('.verified') %>
+ <% elsif api_user.verification_pending_at.present? %>
+ <%= t('.pending_review') %>
+ <% elsif api_user.ident_request_sent_at.present? %>
+ <%= t('.requested') %>
+ <% else %>
+ <%= t('.unverified') %>
+ <% end %>
+ |
<%= api_user.active %> |
<% end %>
diff --git a/app/views/mailers/api_user_mailer/identification_requested.html.erb b/app/views/mailers/api_user_mailer/identification_requested.html.erb
new file mode 100644
index 0000000000..85874e5c07
--- /dev/null
+++ b/app/views/mailers/api_user_mailer/identification_requested.html.erb
@@ -0,0 +1,16 @@
+Tere <%= @api_user.username %>,
+
+Teie registripidaja <%= @api_user.registrar.name %> on palunud Teil kinnitada oma isikut API kasutaja konto jaoks.
+
+" style="display: inline-block; padding: 10px 20px; background-color: #007BFF; color: white; text-decoration: none; border-radius: 5px;">Jätka eeID-ga
+
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+Hi <%= @api_user.username %>,
+
+Your registrar <%= @api_user.registrar.name %> has requested that you verify your identity for your API user account.
+
+" style="display: inline-block; padding: 10px 20px; background-color: #007BFF; color: white; text-decoration: none; border-radius: 5px;">Continue with eeID
+
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/api_user_mailer/identification_requested.text.erb b/app/views/mailers/api_user_mailer/identification_requested.text.erb
new file mode 100644
index 0000000000..847ccae60e
--- /dev/null
+++ b/app/views/mailers/api_user_mailer/identification_requested.text.erb
@@ -0,0 +1,13 @@
+Tere <%= @api_user.username %>,
+
+Teie registripidaja <%= @api_user.registrar.name %> on palunud Teil kinnitada oma isikut API kasutaja konto jaoks.
+
+<%= @verification_link + "?ui_locales=et" %>
+
+---
+
+Hi <%= @api_user.username %>,
+
+Your registrar <%= @api_user.registrar.name %> has requested that you verify your identity for your API user account.
+
+<%= @verification_link + "?ui_locales=en" %>
diff --git a/app/views/mailers/registrar_mailer/api_user_subject_changed.html.erb b/app/views/mailers/registrar_mailer/api_user_subject_changed.html.erb
new file mode 100644
index 0000000000..b79781e7ee
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_subject_changed.html.erb
@@ -0,0 +1,35 @@
+Tere,
+
+
+Anname teada, et järgmise API kasutaja sisselogimise identifikaatorit muudeti.
+Seetõttu nulliti kasutaja isikutuvastuse staatus ja kasutaja peab läbima isikutuvastuse uuesti.
+
+API kasutaja andmed:
+
+ - Kasutajanimi: <%= @api_user.username %>
+ - Varasem identifikaator: <%= @old_subject %>
+ - Uus identifikaator: <%= @new_subject %>
+
+
+Parimate soovidega,
+
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+
+Hi,
+
+
+We are writing to inform you that the login subject for the following API user was changed.
+As a result, the identity verification status was reset and the user must complete verification again.
+
+API user details:
+
+ - Username: <%= @api_user.username %>
+ - Previous subject: <%= @old_subject %>
+ - New subject: <%= @new_subject %>
+
+
+Best regards,
+
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/registrar_mailer/api_user_subject_changed.text.erb b/app/views/mailers/registrar_mailer/api_user_subject_changed.text.erb
new file mode 100644
index 0000000000..60984b70c3
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_subject_changed.text.erb
@@ -0,0 +1,26 @@
+Tere,
+
+Anname teada, et järgmise API kasutaja sisselogimise identifikaatorit muudeti.
+Seetõttu nulliti kasutaja isikutuvastuse staatus ja kasutaja peab läbima isikutuvastuse uuesti.
+
+API kasutaja andmed:
+- Kasutajanimi: <%= @api_user.username %>
+- Varasem identifikaator: <%= @old_subject %>
+- Uus identifikaator: <%= @new_subject %>
+
+Parimate soovidega,
+<%= render 'mailers/shared/signatures/signature.et.text' %>
+---
+
+Hi,
+
+We are writing to inform you that the login subject for the following API user was changed.
+As a result, the identity verification status was reset and the user must complete verification again.
+
+API user details:
+- Username: <%= @api_user.username %>
+- Previous subject: <%= @old_subject %>
+- New subject: <%= @new_subject %>
+
+Best regards,
+<%= render 'mailers/shared/signatures/signature.en.text' %>
diff --git a/app/views/mailers/registrar_mailer/api_user_verification_pending.html.erb b/app/views/mailers/registrar_mailer/api_user_verification_pending.html.erb
new file mode 100644
index 0000000000..ecbb916560
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_verification_pending.html.erb
@@ -0,0 +1,55 @@
+Tere,
+
+
+Anname teada, et järgmise API kasutaja isikutuvastus vajab registripidaja käsitsi kinnitust.
+Automaatne kinnitamine ei olnud võimalik (nt isikukood puudus tulemuses või andmed ei ole üheselt seostatavad).
+
+API kasutaja andmed:
+
+ - Kasutajanimi: <%= @api_user.username %>
+
+<% if @verification_snapshot.present? %>
+ eeID tulemus:
+
+ <% @verification_snapshot.each do |key, value| %>
+ <% next if value.blank? %>
+ - <%= key %>: <%= value %>
+ <% end %>
+
+<% end %>
+
+Palun kinnitage või lükake isikutuvastus tagasi registripidaja portaalis.
+
+
+Parimate soovidega,
+
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+
+Hi,
+
+
+We are writing to inform you that the identity verification for the following API user requires manual registrar confirmation.
+Automatic verification was not possible (e.g. identity code missing in the result or data not unambiguously matchable).
+
+API user details:
+
+ - Username: <%= @api_user.username %>
+
+<% if @verification_snapshot.present? %>
+ eeID result:
+
+ <% @verification_snapshot.each do |key, value| %>
+ <% next if value.blank? %>
+ - <%= key %>: <%= value %>
+ <% end %>
+
+<% end %>
+
+Please confirm or reject the identity verification in the Registrar Portal.
+
+
+Best regards,
+
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/registrar_mailer/api_user_verification_pending.text.erb b/app/views/mailers/registrar_mailer/api_user_verification_pending.text.erb
new file mode 100644
index 0000000000..ca67ce70bd
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_verification_pending.text.erb
@@ -0,0 +1,34 @@
+Tere,
+
+Anname teada, et järgmise API kasutaja isikutuvastus vajab registripidaja käsitsi kinnitust.
+Automaatne kinnitamine ei olnud võimalik (nt isikukood puudus tulemuses või andmed ei ole üheselt seostatavad).
+
+API kasutaja andmed:
+- Kasutajanimi: <%= @api_user.username %>
+<% if @verification_snapshot.present? %>
+eeID tulemus:
+<% @verification_snapshot.each do |key, value| %><% next if value.blank? %>- <%= key %>: <%= value %>
+<% end %>
+<% end %>
+Palun kinnitage või lükake isikutuvastus tagasi registripidaja portaalis.
+
+Parimate soovidega,
+<%= render 'mailers/shared/signatures/signature.et.text' %>
+---
+
+Hi,
+
+We are writing to inform you that the identity verification for the following API user requires manual registrar confirmation.
+Automatic verification was not possible (e.g. identity code missing in the result or data not unambiguously matchable).
+
+API user details:
+- Username: <%= @api_user.username %>
+<% if @verification_snapshot.present? %>
+eeID result:
+<% @verification_snapshot.each do |key, value| %><% next if value.blank? %>- <%= key %>: <%= value %>
+<% end %>
+<% end %>
+Please confirm or reject the identity verification in the Registrar Portal.
+
+Best regards,
+<%= render 'mailers/shared/signatures/signature.en.text' %>
diff --git a/app/views/mailers/registrar_mailer/api_user_verified.html.erb b/app/views/mailers/registrar_mailer/api_user_verified.html.erb
new file mode 100644
index 0000000000..133e81abb8
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_verified.html.erb
@@ -0,0 +1,37 @@
+Tere,
+
+
+Anname teada, et järgmise API kasutaja isikutuvastus on automaatselt edukalt lõpule viidud.
+
+API kasutaja andmed:
+
+ - Kasutajanimi: <%= @api_user.username %>
+ - Isikukood: <%= @api_user.identity_code %>
+
+
+Täielikud tulemused leiate manuses olevast PDF-failist.
+
+
+Parimate soovidega,
+
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+
+Hi,
+
+
+We are writing to inform you that the identity verification for the following API user has been automatically completed.
+
+API user details:
+
+ - Username: <%= @api_user.username %>
+ - Identity code: <%= @api_user.identity_code %>
+
+
+The full result can be found in the attached PDF file.
+
+
+Best regards,
+
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/registrar_mailer/api_user_verified.text.erb b/app/views/mailers/registrar_mailer/api_user_verified.text.erb
new file mode 100644
index 0000000000..39807454e5
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/api_user_verified.text.erb
@@ -0,0 +1,26 @@
+Tere,
+
+Anname teada, et järgmise API kasutaja isikutuvastus on automaatselt edukalt lõpule viidud.
+
+API kasutaja andmed:
+- Kasutajanimi: <%= @api_user.username %>
+- Isikukood: <%= @api_user.identity_code %>
+
+Täielikud tulemused leiate manuses olevast PDF-failist.
+
+Parimate soovidega,
+<%= render 'mailers/shared/signatures/signature.et.text' %>
+---
+
+Hi,
+
+We are writing to inform you that the identity verification for the following API user has been automatically completed.
+
+API user details:
+- Username: <%= @api_user.username %>
+- Identity code: <%= @api_user.identity_code %>
+
+The full result can be found in the attached PDF file.
+
+Best regards,
+<%= render 'mailers/shared/signatures/signature.en.text' %>
diff --git a/app/views/mailers/registrar_mailer/contact_verification_pending.html.erb b/app/views/mailers/registrar_mailer/contact_verification_pending.html.erb
new file mode 100644
index 0000000000..c9563c07e6
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/contact_verification_pending.html.erb
@@ -0,0 +1,57 @@
+Tere,
+
+
+Anname teada, et järgmise kontakti isikutuvastus vajab registripidaja käsitsi kinnitust.
+Automaatne kinnitamine ei olnud võimalik (nt isikukood puudus tulemuses või andmed ei ühti kontakti andmetega).
+
+Kontakti andmed:
+
+ - Kontakti kood: <%= @contact.code %>
+ - Nimi: <%= @contact.name %>
+
+<% if @verification_snapshot.present? %>
+ eeID tulemus:
+
+ <% @verification_snapshot.each do |key, value| %>
+ <% next if value.blank? %>
+ - <%= key %>: <%= value %>
+ <% end %>
+
+<% end %>
+
+Palun kinnitage või lükake isikutuvastus tagasi registripidaja portaalis.
+
+
+Parimate soovidega,
+
+<%= render 'mailers/shared/signatures/signature.et.html' %>
+
+
+
+Hi,
+
+
+We are writing to inform you that the identity verification for the following contact requires manual registrar confirmation.
+Automatic verification was not possible (e.g. identity code missing in the result or data does not match the contact record).
+
+Contact details:
+
+ - Contact code: <%= @contact.code %>
+ - Name: <%= @contact.name %>
+
+<% if @verification_snapshot.present? %>
+ eeID result:
+
+ <% @verification_snapshot.each do |key, value| %>
+ <% next if value.blank? %>
+ - <%= key %>: <%= value %>
+ <% end %>
+
+<% end %>
+
+Please confirm or reject the identity verification in the Registrar Portal.
+
+
+Best regards,
+
+<%= render 'mailers/shared/signatures/signature.en.html' %>
diff --git a/app/views/mailers/registrar_mailer/contact_verification_pending.text.erb b/app/views/mailers/registrar_mailer/contact_verification_pending.text.erb
new file mode 100644
index 0000000000..88d325ba85
--- /dev/null
+++ b/app/views/mailers/registrar_mailer/contact_verification_pending.text.erb
@@ -0,0 +1,45 @@
+Tere,
+
+Anname teada, et järgmise kontakti isikutuvastus vajab registripidaja käsitsi kinnitust.
+Automaatne kinnitamine ei olnud võimalik (nt isikukood puudus tulemuses või andmed ei ühti kontakti andmetega).
+
+Kontakti kood: <%= @contact.code %>
+Nimi: <%= @contact.name %>
+
+<% if @verification_snapshot.present? %>
+eeID tulemus:
+<% @verification_snapshot.each do |key, value| %>
+<% next if value.blank? %>
+<%= key %>: <%= value %>
+<% end %>
+<% end %>
+
+Palun kinnitage või lükake isikutuvastus tagasi registripidaja portaalis.
+
+Parimate soovidega,
+
+<%= render 'mailers/shared/signatures/signature.et.text' %>
+
+---
+
+Hi,
+
+We are writing to inform you that the identity verification for the following contact requires manual registrar confirmation.
+Automatic verification was not possible (e.g. identity code missing in the result or data does not match the contact record).
+
+Contact code: <%= @contact.code %>
+Name: <%= @contact.name %>
+
+<% if @verification_snapshot.present? %>
+eeID result:
+<% @verification_snapshot.each do |key, value| %>
+<% next if value.blank? %>
+<%= key %>: <%= value %>
+<% end %>
+<% end %>
+
+Please confirm or reject the identity verification in the Registrar Portal.
+
+Best regards,
+
+<%= render 'mailers/shared/signatures/signature.en.text' %>
diff --git a/config/locales/accounts.en.yml b/config/locales/accounts.en.yml
new file mode 100644
index 0000000000..a749c8aefe
--- /dev/null
+++ b/config/locales/accounts.en.yml
@@ -0,0 +1,21 @@
+en:
+ activerecord:
+ models:
+ account:
+ one: Account
+ other: Accounts
+ attributes:
+ account:
+ account_type: Account type
+ registrar: Registrar
+ balance: Balance
+ currency: Currency
+ errors:
+ models:
+ account:
+ attributes:
+ account_type:
+ blank: Account type is missing
+ registrar:
+ blank: Registrar is missing
+ required: Registrar is missing
diff --git a/config/locales/accounts.et.yml b/config/locales/accounts.et.yml
new file mode 100644
index 0000000000..0993a5cda2
--- /dev/null
+++ b/config/locales/accounts.et.yml
@@ -0,0 +1,21 @@
+et:
+ activerecord:
+ models:
+ account:
+ one: Konto
+ other: Kontod
+ attributes:
+ account:
+ account_type: Kontotüüp
+ registrar: Registraator
+ balance: Saldo
+ currency: Valuuta
+ errors:
+ models:
+ account:
+ attributes:
+ account_type:
+ blank: Kontotüüp on kohustuslik
+ registrar:
+ blank: Registraator on kohustuslik
+ required: Registraator on kohustuslik
diff --git a/config/locales/admin/registrars.en.yml b/config/locales/admin/registrars.en.yml
index 0550650586..e02d18b77a 100644
--- a/config/locales/admin/registrars.en.yml
+++ b/config/locales/admin/registrars.en.yml
@@ -30,6 +30,11 @@ en:
api_users:
header: API Users
new_btn: New API user
+ verification_status: Verification status
+ verified: Verified
+ pending_review: Pending registrar review
+ requested: Requested
+ unverified: Unverified
set_test_btn: Set Accreditation
remove_test_btn: Remove Accreditation
diff --git a/config/locales/api_users.en.yml b/config/locales/api_users.en.yml
index 9d4fcb63a5..879c515aa9 100644
--- a/config/locales/api_users.en.yml
+++ b/config/locales/api_users.en.yml
@@ -1,6 +1,40 @@
en:
activerecord:
+ models:
+ api_user:
+ one: API user
+ other: API users
attributes:
api_user:
+ username: Username
plain_text_password: Password
- roles: Role
+ email: Email
+ identity_code: Identity code
+ subject: Login subject
+ registrar: Registrar
+ roles: Roles
+ errors:
+ models:
+ api_user:
+ attributes:
+ username:
+ blank: is missing
+ taken: already exists
+ plain_text_password:
+ blank: is missing
+ too_short: is too short (minimum is %{count} characters)
+ email:
+ blank: is required for identity verification
+ invalid: is invalid
+ identity_code:
+ taken: already exists at this registrar
+ subject:
+ taken: is already used by another api user at this registrar
+ registrar:
+ blank: is missing
+ roles:
+ blank: is missing
+ base:
+ verification_error: Sending identification request failed
+ not_pending_verification: No pending verification to approve or reject
+ missing_subject: Login subject is required to approve verification
diff --git a/config/locales/api_users.et.yml b/config/locales/api_users.et.yml
new file mode 100644
index 0000000000..f12850cc94
--- /dev/null
+++ b/config/locales/api_users.et.yml
@@ -0,0 +1,40 @@
+et:
+ activerecord:
+ models:
+ api_user:
+ one: API kasutaja
+ other: API kasutajad
+ attributes:
+ api_user:
+ username: Kasutajanimi
+ plain_text_password: Parool
+ email: E-post
+ identity_code: Isikukood
+ subject: Sisselogimise identifikaator
+ registrar: Registripidaja
+ roles: Rollid
+ errors:
+ models:
+ api_user:
+ attributes:
+ username:
+ blank: on kohustuslik
+ taken: on juba kasutusel
+ plain_text_password:
+ blank: on kohustuslik
+ too_short: on liiga lühike (peab olema vähemalt %{count} tähemärki)
+ email:
+ blank: on vajalik isikutuvastamiseks
+ invalid: on vigane
+ identity_code:
+ taken: on selle registripidaja juures juba olemas
+ subject:
+ taken: on selle registripidaja juures juba teise API kasutaja poolt kasutusel
+ registrar:
+ blank: on kohustuslik
+ roles:
+ blank: on kohustuslik
+ base:
+ verification_error: Identifitseerimistaotluse saatmine ebaõnnestus
+ not_pending_verification: Kinnitamist ootavat identifitseerimist pole
+ missing_subject: Sisselogimise identifikaator on kinnitamiseks kohustuslik
diff --git a/config/locales/contacts.en.yml b/config/locales/contacts.en.yml
index 86576b00c4..489c3d5b40 100644
--- a/config/locales/contacts.en.yml
+++ b/config/locales/contacts.en.yml
@@ -5,36 +5,74 @@ en:
registrant: Registrant
admin_domain_contact: Administrative contact
tech_domain_contact: Technical contact
+ contact:
+ one: Contact
+ other: Contacts
+ attributes:
+ contact:
+ name: Name
+ email: Email
+ phone: Phone
+ street: Street
+ city: City
+ zip: Postcode
+ country_code: Country code
+ code: Contact code
+ registrant_publishable: Registrant publishable
+ disclosed_attributes: Disclosed attributes
+ statuses: Statuses
+ domains: Domains
errors:
+ messages:
+ epp_id_taken: Contact id already exists
models:
contact:
attributes:
base:
verification_exists: Contact already verified
verification_error: Sending identification request failed
+ not_pending_verification: No pending verification to approve or reject
+ missing_subject: Identity subject is required to approve verification
+ missing_claims: Required identity claims are missing to approve verification
+ id_number_requires_priv_contact: Identification returned a national ID number. Birthday contacts cannot be approved with an ID number
code:
- blank: "Required parameter missing - code"
- too_long_contact_code: "Contact code is too long, max 100 characters"
+ blank: Required parameter missing - code
+ invalid: Contact code is invalid
+ too_long_contact_code: Contact code is too long, max 100 characters
+ epp_id_taken: Contact id already exists
name:
- blank: "Required parameter missing - name"
- invalid: "Name is invalid"
- too_long_contact_name: "Contact name is too long, max 255 characters"
+ blank: Required parameter missing - name
+ invalid: Name is invalid
+ too_long_contact_name: Contact name is too long, max 255 characters
phone:
- blank: "Required parameter missing - phone"
- invalid: "Phone nr is invalid"
+ blank: Required parameter missing - phone
+ invalid: Phone nr is invalid
+ too_long: Phone nr is too long, max 17 characters
email:
- blank: "Required parameter missing - email"
- invalid: "Email is invalid"
+ blank: Required parameter missing - email
+ invalid: Email is invalid
email_smtp_check_error: SMTP check error
email_mx_check_error: Mail domain not found
email_regex_check_error: Invalid format
+ street:
+ blank: Required parameter missing - street
+ invalid: Street is invalid
+ city:
+ blank: Required parameter missing - city
+ invalid: City is invalid
+ zip:
+ blank: Required parameter missing - zip
+ invalid: Postcode is invalid
domains:
- exist: 'Object association prohibits operation'
+ exist: Object association prohibits operation
delete_prohibited: Contact delete prohibited by status
statuses:
- not_uniq: 'not uniq'
+ not_uniq: not uniq
delete_prohibited: Contact delete prohibited by status
country_code:
+ blank: Required parameter missing - country code
invalid: Country code is not valid, should be in ISO_3166-1 alpha 2 format (%{value})
disclosed_attributes:
invalid: contain unsupported attribute(s)
+ registrant_publishable:
+ inclusion: is not included in the list
diff --git a/config/locales/contacts.et.yml b/config/locales/contacts.et.yml
index 5cbaeb06a8..d3acbe9c79 100644
--- a/config/locales/contacts.et.yml
+++ b/config/locales/contacts.et.yml
@@ -5,3 +5,74 @@ et:
registrant: Registreerija
admin_domain_contact: Halduskontakt
tech_domain_contact: Tehniline kontakt
+ contact:
+ one: Kontakt
+ other: Kontaktid
+ attributes:
+ contact:
+ name: Nimi
+ email: E-post
+ phone: Telefon
+ street: Tänav
+ city: Linn
+ zip: Postiindeks
+ country_code: Riigikood
+ code: Kontakti kood
+ registrant_publishable: Registreerija avalikustatav
+ disclosed_attributes: Avalikustatavad andmed
+ statuses: Staatused
+ domains: Domeenid
+ errors:
+ messages:
+ epp_id_taken: Kontakti ID on juba olemas
+ models:
+ contact:
+ attributes:
+ base:
+ verification_exists: Kontakt on juba kinnitatud
+ verification_error: Identifitseerimistaotluse saatmine ebaõnnestus
+ not_pending_verification: Kinnitamist ootavat identifitseerimist pole
+ missing_subject: Isiku identifikaator on kinnitamiseks kohustuslik
+ missing_claims: Kinnitamiseks vajalikud isikuandmed puuduvad
+ id_number_requires_priv_contact: Isikutuvastus tagastas isikukoodi. Sünnikuupäeva tüüpi kontakti ei saa isikukoodiga kinnitada
+ code:
+ blank: Kohustuslik parameeter puudub – kood
+ invalid: Kontakti kood on vigane
+ too_long_contact_code: Kontakti kood on liiga pikk, maksimaalselt 100 tähemärki
+ epp_id_taken: Kontakti ID on juba olemas
+ name:
+ blank: Kohustuslik parameeter puudub – nimi
+ invalid: Nimi on vigane
+ too_long_contact_name: Kontakti nimi on liiga pikk, maksimaalselt 255 tähemärki
+ phone:
+ blank: Kohustuslik parameeter puudub – telefon
+ invalid: Telefoninumber on vigane
+ too_long: Telefoninumber on liiga pikk, maksimaalselt 17 tähemärki
+ email:
+ blank: Kohustuslik parameeter puudub – e-post
+ invalid: E-posti aadress on vigane
+ email_smtp_check_error: SMTP kontrolli viga
+ email_mx_check_error: Meilidomeeni ei leitud
+ email_regex_check_error: Vigane vorming
+ street:
+ blank: Kohustuslik parameeter puudub – tänav
+ invalid: Tänav on vigane
+ city:
+ blank: Kohustuslik parameeter puudub – linn
+ invalid: Linn on vigane
+ zip:
+ blank: Kohustuslik parameeter puudub – postiindeks
+ invalid: Postiindeks on vigane
+ domains:
+ exist: Objekti seos keelab toimingu
+ delete_prohibited: Kontakti kustutamine on staatuse tõttu keelatud
+ statuses:
+ not_uniq: pole unikaalne
+ delete_prohibited: Kontakti kustutamine on staatuse tõttu keelatud
+ country_code:
+ blank: Kohustuslik parameeter puudub – riigikood
+ invalid: Riigikood on vigane, peab olema ISO 3166-1 alpha-2 vormingus (%{value})
+ disclosed_attributes:
+ invalid: sisaldab toetamata atribuute
+ registrant_publishable:
+ inclusion: ei ole lubatud väärtuste hulgas
diff --git a/config/locales/domains.en.yml b/config/locales/domains.en.yml
new file mode 100644
index 0000000000..c5b7e13837
--- /dev/null
+++ b/config/locales/domains.en.yml
@@ -0,0 +1,133 @@
+en:
+ activerecord:
+ models:
+ domain:
+ one: Domain
+ other: Domains
+ attributes:
+ epp_domain: &epp_domain_attributes
+ name_dirty: Domain name
+ name: Domain name
+ name_puny: Domain name
+ puny_label: Domain name
+ period: Period
+ transfer_code: Transfer code
+ registrant: Registrant
+ registrar: Registrar
+ nameservers: Nameservers
+ dnskeys: DNS keys
+ admin_domain_contacts: Admin contacts
+ admin_contacts: Admin contacts
+ tech_domain_contacts: Technical contacts
+ tech_contacts: Technical contacts
+ domain_contacts: Contacts
+ statuses: Statuses
+ domain_statuses: Statuses
+ reserved_pw: Reserved password
+ domain:
+ <<: *epp_domain_attributes
+ nameserver:
+ hostname: Hostname
+ ipv4: IPv4
+ ipv6: IPv6
+ domain_contact:
+ contact: Contact
+ domain_status:
+ value: Status
+ errors:
+ models:
+ epp_domain: &epp_domain_errors
+ attributes:
+ base:
+ domain_status_prohibits_operation: Domain status prohibits operation
+ domain_already_belongs_to_the_querying_registrar: Domain already belongs to the querying registrar
+ ds_data_not_allowed: dsData object is not allowed
+ ds_data_with_key_not_allowed: dsData object with key data is not allowed
+ key_data_not_allowed: keyData object is not allowed
+ required_parameter_missing_reserved: Required parameter missing; reserved>pw element required for reserved domains
+ invalid_auth_information_reserved: Invalid authorization information; invalid reserved>pw value
+ required_parameter_missing_disputed: Required parameter missing; disputed pw element required for dispute domains
+ invalid_auth_information_disputed: Invalid authorization information; invalid disputed>pw value
+ dispute_update_requires_registrant_change: Object status prohibits operation; disputed domain update requires registrant change
+ domain_name_blocked: 'Data management policy violation: Domain name is blocked [name]'
+ session_limit_exceeded: Session limit exceeded; server closing connection
+ domain_at_auction: 'Parameter value policy error: domain is at auction'
+ reserved_pw_element_required: 'Required parameter missing; reserved>pw element is required'
+ name_dirty:
+ blank: Domain name is missing
+ invalid: Domain name is invalid
+ reserved: Domain name is reserved
+ taken: Domain name already exists
+ blocked: Domain name is blocked
+ puny_label:
+ too_long: Domain name is too long (maximum is 63 characters)
+ period:
+ blank: Period is missing
+ not_a_number: Period is not a number
+ not_an_integer: Period must be an integer
+ transfer_code:
+ blank: Transfer code is missing
+ wrong_pw: Authorization error
+ registrant:
+ blank: Registrant is missing
+ not_found: Registrant not found
+ cannot_be_missing: 'Parameter value policy error: registrant cannot be missing'
+ domain_contacts:
+ invalid: Contacts are invalid
+ not_found: Contact was not found
+ admin_contact_can_be_only_private_person: Admin contact can be private person only
+ admin_domain_contacts:
+ out_of_range: Admin contacts count must be between %{min}-%{max}
+ admin_contacts:
+ out_of_range: Admin contacts count must be between %{min}-%{max}
+ less_than_or_equal_to: Admin contacts count must be less than or equal to %{count}
+ greater_than_or_equal_to: Admin contacts count must be greater than or equal to %{count}
+ invalid_ident_type: Admin contact can be private person only
+ tech_domain_contacts:
+ out_of_range: Tech contacts count must be between %{min}-%{max}
+ tech_contacts:
+ out_of_range: Tech contacts count must be between %{min}-%{max}
+ nameservers:
+ invalid: Nameservers are invalid
+ out_of_range: Data management policy violation; Nameserver count must be between %{min}-%{max} for active domains
+ not_found: Nameserver was not found
+ taken: Nameserver already exists on this domain
+ less_than_or_equal_to: Nameservers count must be less than or equal to %{count}
+ greater_than_or_equal_to: Nameservers count must be greater than or equal to %{count}
+ domain_statuses:
+ invalid: Statuses are invalid
+ not_found: Status was not found
+ taken: Status already exists on this domain
+ statuses:
+ taken: Status already exists on this domain
+ registrar:
+ blank: Registrar is missing
+ dnskeys:
+ not_found: DS was not found
+ invalid: DNS keys are invalid
+ out_of_range: DNS keys count must be between %{min}-%{max}
+ domains:
+ invalid: Object status prohibits operation
+ domain:
+ <<: *epp_domain_errors
+ admin_contact_invalid_ident_type: Admin contact can be private person only (ident type %{ident_type} is not allowed)
+ nameserver:
+ attributes:
+ hostname:
+ invalid: Hostname is invalid
+ taken: Nameserver already exists on this domain
+ puny_to_long: Hostname puny label is too long (maximum is 63 characters)
+ ipv4:
+ blank: IP is missing
+ invalid: IPv4 is invalid
+ ipv6:
+ invalid: IPv6 is invalid
+ domain_contact:
+ attributes:
+ contact:
+ blank: Contact was not found
+ taken: Contact already exists on this domain
+ domain_status:
+ attributes:
+ value:
+ taken: Status already exists on this domain
diff --git a/config/locales/domains.et.yml b/config/locales/domains.et.yml
new file mode 100644
index 0000000000..2173c7e881
--- /dev/null
+++ b/config/locales/domains.et.yml
@@ -0,0 +1,132 @@
+et:
+ activerecord:
+ models:
+ domain:
+ one: Domeen
+ other: Domeenid
+ attributes:
+ epp_domain: &epp_domain_attributes
+ name_dirty: Domeeninimi
+ name: Domeeninimi
+ name_puny: Domeeninimi
+ puny_label: Domeeninimi
+ period: Periood
+ transfer_code: Ülekande kood
+ registrant: Registreerija
+ registrar: Registripidaja
+ nameservers: Nimeserverid
+ dnskeys: DNS võtmed
+ admin_domain_contacts: Halduskontaktid
+ admin_contacts: Halduskontaktid
+ tech_domain_contacts: Tehnilised kontaktid
+ tech_contacts: Tehnilised kontaktid
+ domain_contacts: Kontaktid
+ statuses: Staatused
+ domain_statuses: Staatused
+ reserved_pw: Reserveeritud domeeni parool
+ domain:
+ <<: *epp_domain_attributes
+ nameserver:
+ hostname: Hostinimi
+ ipv4: IPv4
+ ipv6: IPv6
+ domain_contact:
+ contact: Kontakt
+ domain_status:
+ value: Staatus
+ errors:
+ models:
+ epp_domain: &epp_domain_errors
+ attributes:
+ base:
+ domain_status_prohibits_operation: Domeeni staatus keelab toimingu
+ domain_already_belongs_to_the_querying_registrar: Domeen kuulub juba päringu teinud registripidajale
+ ds_data_not_allowed: dsData objekt pole lubatud
+ ds_data_with_key_not_allowed: dsData objekt võtmega pole lubatud
+ key_data_not_allowed: keyData objekt pole lubatud
+ required_parameter_missing_reserved: Kohustuslik parameeter puudub; reserveeritud domeenide puhul on nõutav reserved>pw element
+ invalid_auth_information_reserved: Vigane autoriseerimisteave; vigane reserved>pw väärtus
+ required_parameter_missing_disputed: Kohustuslik parameeter puudub; vaidlustatud domeenide puhul on nõutav disputed pw element
+ invalid_auth_information_disputed: Vigane autoriseerimisteave; vigane disputed pw väärtus
+ domain_name_blocked: 'Andmehalduse poliitika rikkumine: domeeninimi on blokeeritud [name]'
+ session_limit_exceeded: Seansi limiit on ületatud; server sulgeb ühenduse
+ domain_at_auction: 'Parameetriväärtuse poliitika viga: domeen on oksjonil'
+ reserved_pw_element_required: 'Kohustuslik parameeter puudub; reserved>pw element on nõutav'
+ name_dirty:
+ blank: Domeeninimi on kohustuslik
+ invalid: Domeeninimi on vigane
+ reserved: Domeeninimi on reserveeritud
+ taken: Domeeninimi on juba olemas
+ blocked: Domeeninimi on blokeeritud
+ puny_label:
+ too_long: Domeeninimi on liiga pikk (maksimaalselt 63 tähemärki)
+ period:
+ blank: Periood on kohustuslik
+ not_a_number: Periood ei ole number
+ not_an_integer: Periood peab olema täisarv
+ transfer_code:
+ blank: Ülekande kood on kohustuslik
+ wrong_pw: Autoriseerimisviga
+ registrant:
+ blank: Registreerija puudub
+ not_found: Registreerijat ei leitud
+ cannot_be_missing: 'Parameetriväärtuse poliitika viga: registreerija ei tohi puududa'
+ domain_contacts:
+ invalid: Kontaktid on vigased
+ not_found: Kontakti ei leitud
+ admin_contact_can_be_only_private_person: Halduskontakt võib olla ainult eraisik
+ admin_domain_contacts:
+ out_of_range: Halduskontaktide arv peab olema vahemikus %{min}–%{max}
+ admin_contacts:
+ out_of_range: Halduskontaktide arv peab olema vahemikus %{min}–%{max}
+ less_than_or_equal_to: Halduskontaktide arv peab olema väiksem või võrdne %{count}-ga
+ greater_than_or_equal_to: Halduskontaktide arv peab olema suurem või võrdne %{count}-ga
+ invalid_ident_type: Halduskontakt võib olla ainult eraisik
+ tech_domain_contacts:
+ out_of_range: Tehniliste kontaktide arv peab olema vahemikus %{min}–%{max}
+ tech_contacts:
+ out_of_range: Tehniliste kontaktide arv peab olema vahemikus %{min}–%{max}
+ nameservers:
+ invalid: Nimeserverid on vigased
+ out_of_range: Andmehalduse poliitika rikkumine; aktiivsete domeenide nimeserverite arv peab olema vahemikus %{min}–%{max}
+ not_found: Nimeserverit ei leitud
+ taken: Nimeserver on sellel domeenil juba olemas
+ less_than_or_equal_to: Nimeserverite arv peab olema väiksem või võrdne %{count}-ga
+ greater_than_or_equal_to: Nimeserverite arv peab olema suurem või võrdne %{count}-ga
+ domain_statuses:
+ invalid: Staatused on vigased
+ not_found: Staatust ei leitud
+ taken: Staatus on sellel domeenil juba olemas
+ statuses:
+ taken: Staatus on sellel domeenil juba olemas
+ registrar:
+ blank: Registripidaja puudub
+ dnskeys:
+ not_found: DS kirjet ei leitud
+ invalid: DNS võtmed on vigased
+ out_of_range: DNS võtmete arv peab olema vahemikus %{min}–%{max}
+ domains:
+ invalid: Objekti staatus keelab toimingu
+ domain:
+ <<: *epp_domain_errors
+ admin_contact_invalid_ident_type: Halduskontakt võib olla ainult eraisik (identifikaatori tüüp %{ident_type} pole lubatud)
+ nameserver:
+ attributes:
+ hostname:
+ invalid: Hostinimi on vigane
+ taken: Nimeserver on sellel domeenil juba olemas
+ puny_to_long: Hostinime puny label on liiga pikk (maksimaalselt 63 tähemärki)
+ ipv4:
+ blank: IP-aadress puudub
+ invalid: IPv4 on vigane
+ ipv6:
+ invalid: IPv6 on vigane
+ domain_contact:
+ attributes:
+ contact:
+ blank: Kontakti ei leitud
+ taken: Kontakt on sellel domeenil juba olemas
+ domain_status:
+ attributes:
+ value:
+ taken: Staatus on sellel domeenil juba olemas
diff --git a/config/locales/en.yml b/config/locales/en.yml
index de7ca58f0c..3dd238e01e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -25,10 +25,10 @@ en:
ds_data_not_allowed: 'dsData object is not allowed'
ds_data_with_key_not_allowed: 'dsData object with key data is not allowed'
key_data_not_allowed: 'keyData object is not allowed'
- required_parameter_missing_reserved: 'Required parameter missing; reserved>pw element required for reserved domains'
- invalid_auth_information_reserved: 'Invalid authorization information; invalid reserved>pw value'
- required_parameter_missing_disputed: 'Required parameter missing; disputed pw element required for dispute domains'
- invalid_auth_information_disputed: 'Invalid authorization information; invalid disputed>pw value'
+ required_parameter_missing_reserved: 'Required parameter missing; reserved_pw element required for reserved domains'
+ invalid_auth_information_reserved: 'Invalid authorization information; invalid reserved_pw value'
+ required_parameter_missing_disputed: 'Required parameter missing; reserved_pw element required for dispute domains'
+ invalid_auth_information_disputed: 'Invalid authorization information; invalid reserved_pw value'
dispute_update_requires_registrant_change: 'Object status prohibits operation; disputed domain update requires registrant change'
domain_name_blocked: 'Data management policy violation: Domain name is blocked [name]'
name_dirty:
@@ -208,6 +208,7 @@ en:
failed_epp_conn: 'Failed to open connection to EPP server!'
epp_conn_error: 'CONNECTION ERROR - Is the EPP server running?'
company_not_registered: 'Company is not registered'
+ invalid_ident_type: Invalid ident type
domain_name_blocked: 'Data management policy violation: Domain name is blocked [name]'
code: 'Code'
action: 'Action'
@@ -406,6 +407,7 @@ en:
internal_error: 'Internal error'
client_side_status_editing_error: 'Parameter value policy error. Client-side object status management not supported'
parameter_value_syntax_error: 'Parameter value syntax error:'
+ email_did_not_pass_validation: "email '%{email}' didn't pass validation"
username: 'Username'
register: 'Register'
diff --git a/config/locales/epp/contacts.et.yml b/config/locales/epp/contacts.et.yml
new file mode 100644
index 0000000000..7cc4b76ab6
--- /dev/null
+++ b/config/locales/epp/contacts.et.yml
@@ -0,0 +1,5 @@
+et:
+ epp:
+ contacts:
+ completed: Käsk edukalt täidetud
+ completed_without_address: Käsk edukalt täidetud; postiaadressi andmed jäeti arvestamata
diff --git a/config/locales/errors.et.yml b/config/locales/errors.et.yml
new file mode 100644
index 0000000000..e72b3bbe91
--- /dev/null
+++ b/config/locales/errors.et.yml
@@ -0,0 +1,6 @@
+et:
+ errors:
+ messages:
+ required_ident_attribute_missing: 'Kohustuslik ident atribuut puudub: %{key}'
+ company_not_registered: Ettevõte pole registreeritud
+ invalid_ident_type: Vigane identi tüüp
diff --git a/config/locales/et.yml b/config/locales/et.yml
index 51192c7087..b28cc5af2c 100644
--- a/config/locales/et.yml
+++ b/config/locales/et.yml
@@ -25,6 +25,14 @@ et:
you_have_a_new_invoice: 'Teil on uus arve.'
monthly_invoice: "Siit tuleb aruanne möödunud kuul ettemaksukontoga seotud tasuliste toimingutega."
sincerely: 'Lugupidamisega'
+ client_side_status_editing_error: 'Parameetriväärtuse poliitika viga. Kliendipoolne objekti staatuse haldus pole toetatud'
+ parameter_value_syntax_error: 'Parameetri väärtuse süntaksiviga:'
+ email_did_not_pass_validation: "e-posti aadress '%{email}' ei läbinud valideerimist"
+ object_status_prohibits_operation: 'Objekti staatus keelab toimingu'
+ domain_already_belongs_to_the_querying_registrar: Domeen kuulub juba päringu teinud registripidajale
+ billing_failure_credit_balance_low: Arveldus ebaõnnestus – ettemaksukonto saldo on liiga madal
+ active_price_missing_for_this_operation: Selle toimingu aktiivne hind puudub!
+ create: Loomine
activerecord:
errors:
diff --git a/config/locales/idents.yml b/config/locales/idents.yml
index 8e6a677de4..31fa3231d9 100644
--- a/config/locales/idents.yml
+++ b/config/locales/idents.yml
@@ -1,5 +1,10 @@
en:
activemodel:
+ attributes:
+ contact/ident:
+ code: Code
+ type: Ident type
+ country_code: Country code
errors:
models:
contact/ident:
@@ -7,6 +12,40 @@ en:
base:
mismatch: Ident type "%{type}" is invalid for %{country}
code:
+ blank: is missing
invalid_national_id: does not conform to national identification number format of %{country}
invalid_reg_no: does not conform to registration number format of %{country}
invalid_birth_date: Birth date is invalid, age must be over 0 and under 150
+ invalid_iso8601_date: has invalid date format YYYY-MM-DD (ISO 8601)
+ type:
+ blank: is missing
+ inclusion: is not included in the list
+ country_code:
+ blank: is missing
+ invalid_iso31661_alpha2: does not conform to ISO 3166-1 alpha-2 standard
+
+et:
+ activemodel:
+ attributes:
+ contact/ident:
+ code: Kood
+ type: Identifikaatori tüüp
+ country_code: Riigikood
+ errors:
+ models:
+ contact/ident:
+ attributes:
+ base:
+ mismatch: Identifikaatori tüüp "%{type}" on kehtetu riigi %{country} jaoks
+ code:
+ blank: on kohustuslik
+ invalid_national_id: ei vasta riigi %{country} isikukoodi vormingule
+ invalid_reg_no: ei vasta riigi %{country} registrinumbri vormingule
+ invalid_birth_date: Sünnikuupäev on vigane, vanus peab olema üle 0 ja alla 150 aasta
+ invalid_iso8601_date: on vigane kuupäeva vorming YYYY-MM-DD (ISO 8601)
+ type:
+ blank: on kohustuslik
+ inclusion: ei ole lubatud väärtuste hulgas
+ country_code:
+ blank: on kohustuslik
+ invalid_iso31661_alpha2: ei vasta ISO 3166-1 alpha-2 standardile
diff --git a/config/locales/invoices.en.yml b/config/locales/invoices.en.yml
new file mode 100644
index 0000000000..7b0418118a
--- /dev/null
+++ b/config/locales/invoices.en.yml
@@ -0,0 +1,48 @@
+en:
+ cannot get access: Cannot get access to billing service
+ failed_to_generate_invoice_invoice_number_limit_reached: Failed to generate invoice - invoice number limit reached
+ invoice_no: 'Invoice no. %{no}'
+ cancelled: Cancelled
+ unpaid: Unpaid
+ activerecord:
+ models:
+ invoice:
+ one: Invoice
+ other: Invoices
+ invoice_item:
+ one: Invoice line
+ other: Invoice lines
+ attributes:
+ invoice:
+ due_date: Due date
+ currency: Currency
+ seller_name: Seller name
+ seller_iban: Seller IBAN
+ buyer_name: Buyer name
+ buyer_vat_no: Buyer VAT no
+ number: Invoice number
+ total: Total
+ items: Invoice items
+ invoice_item:
+ price: Price
+ quantity: Quantity
+ description: Description
+ errors:
+ models:
+ invoice:
+ attributes:
+ base:
+ invoice_status_prohibits_operation: Invoice status prohibits operation
+ invalid_state: 'Invalid state %{status}'
+ due_date:
+ blank: Due date is missing
+ currency:
+ blank: Currency is missing
+ seller_name:
+ blank: Seller name is missing
+ seller_iban:
+ blank: Seller IBAN is missing
+ buyer_name:
+ blank: Buyer name is missing
+ items:
+ blank: Invoice must have at least one line item
diff --git a/config/locales/invoices.et.yml b/config/locales/invoices.et.yml
new file mode 100644
index 0000000000..7c0363cd58
--- /dev/null
+++ b/config/locales/invoices.et.yml
@@ -0,0 +1,48 @@
+et:
+ cannot get access: Arveldusteenusele juurdepääsu ei õnnestunud saada
+ failed_to_generate_invoice_invoice_number_limit_reached: Arve genereerimine ebaõnnestus – arve numbri limiit on täis
+ invoice_no: 'Arve nr %{no}'
+ cancelled: Tühistatud
+ unpaid: Maksmata
+ activerecord:
+ models:
+ invoice:
+ one: Arve
+ other: Arved
+ invoice_item:
+ one: Arve rida
+ other: Arve read
+ attributes:
+ invoice:
+ due_date: Tähtaeg
+ currency: Valuuta
+ seller_name: Müüja nimi
+ seller_iban: Müüja IBAN
+ buyer_name: Ostja nimi
+ buyer_vat_no: Ostja käibemaksukohustuslase number
+ number: Arve number
+ total: Summa
+ items: Arve read
+ invoice_item:
+ price: Hind
+ quantity: Kogus
+ description: Kirjeldus
+ errors:
+ models:
+ invoice:
+ attributes:
+ base:
+ invoice_status_prohibits_operation: Arve staatus keelab toimingu
+ invalid_state: 'Vigane staatus %{status}'
+ due_date:
+ blank: Tähtaeg on kohustuslik
+ currency:
+ blank: Valuuta on kohustuslik
+ seller_name:
+ blank: Müüja nimi on kohustuslik
+ seller_iban:
+ blank: Müüja IBAN on kohustuslik
+ buyer_name:
+ blank: Ostja nimi on kohustuslik
+ items:
+ blank: Arvel peab olema vähemalt üks rida
diff --git a/config/locales/mailers/api_user.en.yml b/config/locales/mailers/api_user.en.yml
new file mode 100644
index 0000000000..a86c733799
--- /dev/null
+++ b/config/locales/mailers/api_user.en.yml
@@ -0,0 +1,6 @@
+en:
+ api_user_mailer:
+ identification_requested:
+ subject: >-
+ Palun kinnitage oma isik API kasutaja kontoga seotud toimingute jaoks
+ / Please verify your identity for API user account operations
diff --git a/config/locales/mailers/api_user.et.yml b/config/locales/mailers/api_user.et.yml
new file mode 100644
index 0000000000..894eaa9918
--- /dev/null
+++ b/config/locales/mailers/api_user.et.yml
@@ -0,0 +1,4 @@
+et:
+ api_user_mailer:
+ identification_requested:
+ subject: Palun kinnitage oma isik API kasutaja kontoga seotud toimingute jaoks
diff --git a/config/locales/mailers/registrar.en.yml b/config/locales/mailers/registrar.en.yml
index a230113ca7..8ce5c86790 100644
--- a/config/locales/mailers/registrar.en.yml
+++ b/config/locales/mailers/registrar.en.yml
@@ -3,4 +3,20 @@ en:
contact_verified:
subject: >-
Teade: Kontakti [%{contact_code}] kinnitamine edukalt lõpule viidud
- / Notification: Contact [%{contact_code}] verification successfully completed
\ No newline at end of file
+ / Notification: Contact [%{contact_code}] verification successfully completed
+ contact_verification_pending:
+ subject: >-
+ Teade: Kontakti [%{contact_code}] isikutuvastus vajab registripidaja kinnitust
+ / Notification: Contact [%{contact_code}] identity verification needs registrar review
+ api_user_verified:
+ subject: >-
+ Teade: API kasutaja [%{username}] isikutuvastus kinnitatud automaatselt
+ / Notification: API user [%{username}] identity verification completed
+ api_user_verification_pending:
+ subject: >-
+ Teade: API kasutaja [%{username}] isikutuvastus vajab registripidaja kinnitust
+ / Notification: API user [%{username}] identity verification needs registrar review
+ api_user_subject_changed:
+ subject: >-
+ Teade: API kasutaja [%{username}] sisselogimise identifikaator muudeti
+ / Notification: API user [%{username}] login subject changed
\ No newline at end of file
diff --git a/config/locales/mailers/registrar.et.yml b/config/locales/mailers/registrar.et.yml
new file mode 100644
index 0000000000..909738742d
--- /dev/null
+++ b/config/locales/mailers/registrar.et.yml
@@ -0,0 +1,10 @@
+et:
+ registrar_mailer:
+ contact_verification_pending:
+ subject: 'Teade: Kontakti [%{contact_code}] isikutuvastus vajab registripidaja kinnitust'
+ api_user_verified:
+ subject: 'Teade: API kasutaja [%{username}] isikutuvastus kinnitatud automaatselt'
+ api_user_verification_pending:
+ subject: 'Teade: API kasutaja [%{username}] isikutuvastus vajab registripidaja kinnitust'
+ api_user_subject_changed:
+ subject: 'Teade: API kasutaja [%{username}] sisselogimise identifikaator muudeti'
diff --git a/config/locales/registrar/account.et.yml b/config/locales/registrar/account.et.yml
new file mode 100644
index 0000000000..3719247f54
--- /dev/null
+++ b/config/locales/registrar/account.et.yml
@@ -0,0 +1,32 @@
+et:
+ registrar:
+ account:
+ show:
+ header: Teie konto
+
+ edit:
+ header: Muuda oma kontot
+
+ form:
+ iban_hint: Vajalik e-arvete saatmiseks panka
+ submit_btn: Salvesta muudatused
+
+ update:
+ saved: Teie konto on uuendatud
+
+ details:
+ header: Andmed
+ edit_btn: Muuda
+
+ linked_users:
+ header: Seotud kasutajad
+ switch_btn: Vaheta
+
+ balance_auto_reload:
+ header: Saldo automaatne täiendamine
+ enabled: Lubatud
+ enabled_state_details: Täienda %{amount}, kui saldo langeb alla %{threshold}
+ disabled: Keelatud
+ enable_btn: Luba
+ disable_btn: Keela
+ edit_btn: Muuda
diff --git a/config/locales/registrar/authorization.en.yml b/config/locales/registrar/authorization.en.yml
index 1e4395cb34..9fa6f24ebf 100644
--- a/config/locales/registrar/authorization.en.yml
+++ b/config/locales/registrar/authorization.en.yml
@@ -2,3 +2,5 @@ en:
registrar:
authorization:
ip_not_allowed: Access denied from IP %{ip}
+ invalid_authorization_information: Invalid authorization information
+ identity_not_verified: Identity verification is required before signing in with a password. Sign in with eeID or complete the verification request first.
diff --git a/config/locales/repp.en.yml b/config/locales/repp.en.yml
new file mode 100644
index 0000000000..0a9eadd8d9
--- /dev/null
+++ b/config/locales/repp.en.yml
@@ -0,0 +1,8 @@
+en:
+ repp:
+ command_completed_successfully: Command completed successfully
+ object_does_not_exist: Object does not exist
+ object_does_not_exist_with_name: "Object does not exist: %{name}"
+ errors:
+ invalid_authorization_information: Invalid authorization information
+ object_not_eligible_for_transfer: Object is not eligible for transfer
diff --git a/config/locales/repp.et.yml b/config/locales/repp.et.yml
new file mode 100644
index 0000000000..548fe54e24
--- /dev/null
+++ b/config/locales/repp.et.yml
@@ -0,0 +1,8 @@
+et:
+ repp:
+ command_completed_successfully: Käsk edukalt täidetud
+ object_does_not_exist: Objekt puudub
+ object_does_not_exist_with_name: "Objekt puudub: %{name}"
+ errors:
+ invalid_authorization_information: Vigane autoriseerimisteave
+ object_not_eligible_for_transfer: Objekt ei ole üleandmiseks sobilik
diff --git a/config/routes.rb b/config/routes.rb
index 7bb2e7c963..05af94cda3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -77,6 +77,8 @@
get 'search(/:id)', to: 'contacts#search'
post 'verify/:id', to: 'contacts#verify'
get 'download_poi/:id', to: 'contacts#download_poi'
+ post 'approve_verification/:id', to: 'contacts#approve_verification'
+ post 'reject_verification/:id', to: 'contacts#reject_verification'
end
end
end
@@ -112,6 +114,12 @@
end
end
resources :api_users, only: %i[index show update create destroy] do
+ collection do
+ post 'verify/:id', to: 'api_users#verify'
+ get 'download_poi/:id', to: 'api_users#download_poi'
+ post 'approve_verification/:id', to: 'api_users#approve_verification'
+ post 'reject_verification/:id', to: 'api_users#reject_verification'
+ end
resources :certificates, only: %i[show] do
member do
get 'download'
diff --git a/db/data/20200608084321_fill_email_verifications.rb b/db/data/20200608084321_fill_email_verifications.rb
index c51570d1b4..d65e3b64a4 100644
--- a/db/data/20200608084321_fill_email_verifications.rb
+++ b/db/data/20200608084321_fill_email_verifications.rb
@@ -1,6 +1,4 @@
class FillEmailVerifications < ActiveRecord::Migration[6.0]
- include Concerns::EmailVerifable
-
def up
# registrar_billing_emails = Registrar.pluck(:billing_email).uniq.reject(&:blank?)
# registrar_emails = Registrar.pluck(:email).uniq.reject(&:blank?)
@@ -17,6 +15,6 @@ def up
end
def down
- EmailAddressVerification.delete_all
+ # EmailAddressVerification table removed in 20220413073315_remove_email_address_verifications
end
end
diff --git a/db/data/20200911104302_fix_typo_in_setting_name.rb b/db/data/20200911104302_fix_typo_in_setting_name.rb
index 9dd2b3b2b9..533b142751 100644
--- a/db/data/20200911104302_fix_typo_in_setting_name.rb
+++ b/db/data/20200911104302_fix_typo_in_setting_name.rb
@@ -1,6 +1,6 @@
class FixTypoInSettingName < ActiveRecord::Migration[6.0]
def up
- setting = Setting.find_by(code: 'request_confrimation_on_registrant_change_enabled')
+ setting = Setting.find_by(code: 'request_confirmation_on_registrant_change_enabled')
setting.update(code: 'request_confirmation_on_registrant_change_enabled')
end
diff --git a/db/data/20260601120100_backfill_api_user_subject.rb b/db/data/20260601120100_backfill_api_user_subject.rb
new file mode 100644
index 0000000000..290ebd5a5b
--- /dev/null
+++ b/db/data/20260601120100_backfill_api_user_subject.rb
@@ -0,0 +1,10 @@
+class BackfillApiUserSubject < ActiveRecord::Migration[6.1]
+ def up
+ result = ApiUsers::SubjectBackfill.run
+ say "ApiUser subject backfill: updated=#{result[:updated]} skipped=#{result[:skipped]}"
+ end
+
+ def down
+ ApiUser.where.not(subject: [nil, '']).update_all(subject: nil)
+ end
+end
diff --git a/db/data_schema.rb b/db/data_schema.rb
index 657376ab90..bb04bd70b0 100644
--- a/db/data_schema.rb
+++ b/db/data_schema.rb
@@ -1 +1 @@
-DataMigrate::Data.define(version: 20201007104651)
+DataMigrate::Data.define(version: 20260601120100)
diff --git a/db/migrate/20260529120000_add_verification_fields_to_users.rb b/db/migrate/20260529120000_add_verification_fields_to_users.rb
new file mode 100644
index 0000000000..e6b0523461
--- /dev/null
+++ b/db/migrate/20260529120000_add_verification_fields_to_users.rb
@@ -0,0 +1,13 @@
+class AddVerificationFieldsToUsers < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def change
+ add_column :users, :ident_request_sent_at, :datetime
+ add_column :users, :verified_at, :datetime
+ add_column :users, :verification_id, :string
+ add_column :users, :verification_pending_at, :datetime
+ add_column :users, :verification_snapshot, :jsonb, default: {}
+ add_index :users, :verified_at, algorithm: :concurrently
+ add_index :users, :verification_pending_at, algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20260601120000_add_subject_to_users.rb b/db/migrate/20260601120000_add_subject_to_users.rb
new file mode 100644
index 0000000000..e0d3ea8796
--- /dev/null
+++ b/db/migrate/20260601120000_add_subject_to_users.rb
@@ -0,0 +1,12 @@
+class AddSubjectToUsers < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def change
+ add_column :users, :subject, :string
+ add_index :users, :subject, algorithm: :concurrently
+ add_index :users, %i[registrar_id subject],
+ unique: true,
+ where: "subject IS NOT NULL AND subject != ''",
+ algorithm: :concurrently
+ end
+end
diff --git a/db/migrate/20260608120000_add_contact_verification_review_fields.rb b/db/migrate/20260608120000_add_contact_verification_review_fields.rb
new file mode 100644
index 0000000000..2e8a174626
--- /dev/null
+++ b/db/migrate/20260608120000_add_contact_verification_review_fields.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddContactVerificationReviewFields < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+
+ def change
+ add_column :contacts, :verification_pending_at, :datetime
+ add_column :contacts, :verification_snapshot, :jsonb, default: {}
+ add_index :contacts, :verification_pending_at, algorithm: :concurrently
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 9061c1f3a3..f9fcd64f8c 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1,8 +1,7 @@
-\restrict OgpBVS4LcDZVV6fZTwmnzAAWL70pwHISsUhjW2gqrf2CNdzHNtVCcfzYehS3JFu
-
+\restrict WmlyozFAnc1c6zHWXudb7s2jRC1uKwHPlCMDikRGHsbNPX1TGBaq3KQ01YXVO8T
-- Dumped from database version 13.4 (Debian 13.4-4.pgdg110+1)
--- Dumped by pg_dump version 13.22 (Debian 13.22-0+deb11u1)
+-- Dumped by pg_dump version 13.23 (Debian 13.23-1.pgdg11+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
@@ -734,12 +733,14 @@ CREATE TABLE public.contacts (
disclosed_attributes character varying[] DEFAULT '{}'::character varying[] NOT NULL,
email_history character varying,
registrant_publishable boolean DEFAULT false,
- 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,
verification_id character varying,
- system_disclosed_attributes character varying[] DEFAULT '{}'::character varying[]
+ checked_company_at timestamp without time zone,
+ company_register_status character varying,
+ system_disclosed_attributes character varying[] DEFAULT '{}'::character varying[],
+ verification_pending_at timestamp without time zone,
+ verification_snapshot jsonb DEFAULT '{}'::jsonb
);
@@ -2649,9 +2650,9 @@ CREATE TABLE public.registrars (
legaldoc_optout boolean DEFAULT false NOT NULL,
legaldoc_optout_comment text,
email_history character varying,
- accept_pdf_invoices boolean DEFAULT true,
accreditation_date timestamp without time zone,
- accreditation_expire_date timestamp without time zone
+ accreditation_expire_date timestamp without time zone,
+ accept_pdf_invoices boolean DEFAULT true
);
@@ -2674,6 +2675,25 @@ CREATE SEQUENCE public.registrars_id_seq
ALTER SEQUENCE public.registrars_id_seq OWNED BY public.registrars.id;
+--
+-- Name: reports_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.reports_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: reports_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.reports_id_seq OWNED BY public.admin_reports.id;
+
+
--
-- Name: repp_logs; Type: TABLE; Schema: public; Owner: -
--
@@ -2760,9 +2780,7 @@ CREATE TABLE public.reserved_domains (
legacy_id integer,
name character varying NOT NULL,
password character varying NOT NULL,
- expire_at timestamp without time zone,
- access_token character varying,
- token_created_at timestamp without time zone
+ expire_at timestamp without time zone
);
@@ -2898,7 +2916,13 @@ CREATE TABLE public.users (
legacy_id integer,
accreditation_date timestamp without time zone,
accreditation_expire_date timestamp without time zone,
- uuid uuid DEFAULT public.gen_random_uuid()
+ uuid uuid DEFAULT public.gen_random_uuid(),
+ ident_request_sent_at timestamp without time zone,
+ verified_at timestamp without time zone,
+ verification_id character varying,
+ verification_pending_at timestamp without time zone,
+ verification_snapshot jsonb DEFAULT '{}'::jsonb,
+ subject character varying
);
@@ -3129,7 +3153,7 @@ ALTER TABLE ONLY public.actions ALTER COLUMN id SET DEFAULT nextval('public.acti
-- Name: admin_reports id; Type: DEFAULT; Schema: public; Owner: -
--
-ALTER TABLE ONLY public.admin_reports ALTER COLUMN id SET DEFAULT nextval('public.admin_reports_id_seq'::regclass);
+ALTER TABLE ONLY public.admin_reports ALTER COLUMN id SET DEFAULT nextval('public.reports_id_seq'::regclass);
--
@@ -4425,6 +4449,13 @@ CREATE INDEX index_contacts_on_registrar_id ON public.contacts USING btree (regi
CREATE INDEX index_contacts_on_registrar_id_and_ident_type ON public.contacts USING btree (registrar_id, ident_type);
+--
+-- Name: index_contacts_on_verification_pending_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_contacts_on_verification_pending_at ON public.contacts USING btree (verification_pending_at);
+
+
--
-- Name: index_contacts_on_verified_at; Type: INDEX; Schema: public; Owner: -
--
@@ -4985,6 +5016,34 @@ CREATE INDEX index_users_on_identity_code ON public.users USING btree (identity_
CREATE INDEX index_users_on_registrar_id ON public.users USING btree (registrar_id);
+--
+-- Name: index_users_on_registrar_id_and_subject; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_users_on_registrar_id_and_subject ON public.users USING btree (registrar_id, subject) WHERE ((subject IS NOT NULL) AND ((subject)::text <> ''::text));
+
+
+--
+-- Name: index_users_on_subject; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_users_on_subject ON public.users USING btree (subject);
+
+
+--
+-- Name: index_users_on_verification_pending_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_users_on_verification_pending_at ON public.users USING btree (verification_pending_at);
+
+
+--
+-- Name: index_users_on_verified_at; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_users_on_verified_at ON public.users USING btree (verified_at);
+
+
--
-- Name: index_validation_events_on_event_data; Type: INDEX; Schema: public; Owner: -
--
@@ -5296,12 +5355,11 @@ ALTER TABLE ONLY public.users
-- PostgreSQL database dump complete
--
-\unrestrict OgpBVS4LcDZVV6fZTwmnzAAWL70pwHISsUhjW2gqrf2CNdzHNtVCcfzYehS3JFu
+\unrestrict WmlyozFAnc1c6zHWXudb7s2jRC1uKwHPlCMDikRGHsbNPX1TGBaq3KQ01YXVO8T
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
-('0'),
('20140616073945'),
('20140620130107'),
('20140627082711'),
@@ -5780,19 +5838,14 @@ INSERT INTO "schema_migrations" (version) VALUES
('20221214073933'),
('20221214074252'),
('20230531111154'),
-('20230612094319'),
-('20230612094326'),
-('20230612094335'),
('20230707084741'),
('20230710120154'),
('20230711083811'),
-('20240722085530'),
-('20240723110208'),
('20240816091049'),
('20240816092636'),
+('20240903131540'),
('20240924103554'),
('20241015071505'),
-('20241022121525'),
('20241030095636'),
('20241104104620'),
('20241112093540'),
@@ -5803,9 +5856,13 @@ INSERT INTO "schema_migrations" (version) VALUES
('20250219102811'),
('20250310133151'),
('20250313122119'),
-('20250314133357'),
('20250319104749'),
('20250627084536'),
-('20260406125446'),
('20251230104312'),
-('20260220111500');
+('20260220111500'),
+('20260406125446'),
+('20260529120000'),
+('20260601120000'),
+('20260608120000');
+
+
diff --git a/lib/serializers/repp/api_user.rb b/lib/serializers/repp/api_user.rb
index 2f57df1f67..4b98e3c0fb 100644
--- a/lib/serializers/repp/api_user.rb
+++ b/lib/serializers/repp/api_user.rb
@@ -14,12 +14,19 @@ def to_json(obj = user)
name: obj.username,
password: obj.plain_text_password,
identity_code: obj.identity_code,
+ subject: obj.subject,
+ email: obj.email,
roles: obj.roles.join(', '),
active: obj.active,
created_at: obj.created_at,
updated_at: obj.updated_at,
creator: obj.creator_str,
updator: obj.updator_str,
+ ident_request_sent_at: obj.ident_request_sent_at,
+ verified_at: obj.verified_at,
+ verification_id: obj.verification_id,
+ verification_pending_at: obj.verification_pending_at,
+ verification_snapshot: obj.verification_snapshot
}
json[:certificates] = certificates
json
@@ -34,10 +41,10 @@ def certificates
{ id: x.id, subject: subject_str, status: x.status }
end
end
-
+
def extract_subject(certificate)
subject = nil
-
+
if certificate.csr.present?
begin
if certificate.parsed_csr
@@ -47,7 +54,7 @@ def extract_subject(certificate)
Rails.logger.warn("Error extracting subject from CSR: #{e.message}")
end
end
-
+
if subject.blank? && certificate.crt.present?
begin
if certificate.parsed_crt
@@ -57,7 +64,7 @@ def extract_subject(certificate)
Rails.logger.warn("Error extracting subject from CRT: #{e.message}")
end
end
-
+
subject.presence || certificate.common_name.presence || 'Unknown'
end
end
diff --git a/lib/serializers/repp/contact.rb b/lib/serializers/repp/contact.rb
index a6f7fb2c92..00431d6ab1 100644
--- a/lib/serializers/repp/contact.rb
+++ b/lib/serializers/repp/contact.rb
@@ -19,7 +19,9 @@ def to_json(obj = contact)
created_at: obj.created_at, auth_info: obj.auth_info, email: obj.email,
statuses: statuses, disclosed_attributes: obj.disclosed_attributes,
registrar: registrar, ident_request_sent_at: obj.ident_request_sent_at,
- verified_at: obj.verified_at, verification_id: obj.verification_id }
+ verified_at: obj.verified_at, verification_id: obj.verification_id,
+ verification_pending_at: obj.verification_pending_at,
+ verification_snapshot: obj.verification_snapshot }
json[:address] = address if @show_address
if @domain_params
json[:domains] = domains
diff --git a/lib/tasks/api_users/backfill_subject.rake b/lib/tasks/api_users/backfill_subject.rake
new file mode 100644
index 0000000000..ed3682d585
--- /dev/null
+++ b/lib/tasks/api_users/backfill_subject.rake
@@ -0,0 +1,7 @@
+namespace :api_users do
+ desc 'Backfill subject from identity_code for ApiUsers (idempotent)'
+ task backfill_subject: :environment do
+ result = ApiUsers::SubjectBackfill.run
+ puts "ApiUser subject backfill: updated=#{result[:updated]} skipped=#{result[:skipped]}"
+ end
+end
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index ed05e632dd..941546f30a 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -2,6 +2,8 @@ api_bestnames:
username: test_bestnames
plain_text_password: testtest
identity_code: 1234
+ subject: EE1234
+ verified_at: <%= Time.zone.parse('2024-01-01 12:00:00') %>
type: ApiUser
registrar: bestnames
active: true
@@ -21,6 +23,7 @@ api_bestnames_epp:
api_goodnames:
username: test_goodnames
plain_text_password: testtest
+ verified_at: <%= Time.zone.parse('2024-01-01 12:00:00') %>
type: ApiUser
registrar: goodnames
active: true
@@ -44,6 +47,7 @@ registrant:
accr_bot:
username: accr_bot
plain_text_password: accr_bot
+ verified_at: <%= Time.zone.parse('2024-01-01 12:00:00') %>
type: ApiUser
registrar: goodnames
active: true
diff --git a/test/integration/admin_area/certificates_test.rb b/test/integration/admin_area/certificates_test.rb
index 99e4bee9bf..0363f22cb5 100644
--- a/test/integration/admin_area/certificates_test.rb
+++ b/test/integration/admin_area/certificates_test.rb
@@ -57,7 +57,7 @@ def test_new_api_user
fill_in 'Username', with: 'testapiuser'
fill_in 'Password', with: 'secretpassword'
- fill_in 'Identity code', with: '60305062718'
+ fill_in 'Login subject', with: 'EE60305062718'
click_on 'Create API user'
diff --git a/test/integration/eeid/identification_requests_webhook_test.rb b/test/integration/eeid/identification_requests_webhook_test.rb
index e8a1e2793f..98374e96a4 100644
--- a/test/integration/eeid/identification_requests_webhook_test.rb
+++ b/test/integration/eeid/identification_requests_webhook_test.rb
@@ -16,9 +16,16 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
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})
+ pdf_content = File.binread(Rails.root.join('test/fixtures/files/legaldoc.pdf'))
+ stub_request(:get, %r{api/ident/v1/identification_requests/.+/proof_of_identity})
.to_return(status: 200, body: pdf_content, headers: { 'Content-Type' => 'application/pdf' })
+ default_ident_body = {
+ id: '123',
+ status: 'completed',
+ result: { sub: 'US1234' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/[^/]+$})
+ .to_return(status: 200, body: default_ident_body, headers: { 'Content-Type' => 'application/json' })
adapter = ENV['shunter_default_adapter'].constantize.new
adapter&.clear!
@@ -26,6 +33,14 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
test 'should verify contact with valid signature and parameters' do
@contact.update!(ident_request_sent_at: Time.zone.now - 1.day)
+ ident_body = {
+ id: '123',
+ status: 'completed',
+ result: { sub: 'US1234' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/123$})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
post '/eeid/webhooks/identification_requests', params: { identification_request_id: '123', reference: @contact.code }, as: :json, headers: { 'X-HMAC-Signature' => @valid_hmac_signature }
assert_response :ok
@@ -55,8 +70,12 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
end
test 'should handle internal server error gracefully' do
- # Simulate an error in the verify_contact method
- Contact.stub :find_by_code, ->(_) { raise StandardError, 'Simulated error' } do
+ @contact.update!(ident_request_sent_at: Time.zone.now - 1.day)
+
+ Eeid::Webhooks::IdentificationRequestsController.stub_any_instance(
+ :process_contact,
+ proc { |_contact| raise StandardError, 'Simulated error' }
+ ) do
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
@@ -66,7 +85,7 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
end
test 'should handle error from ident response' do
- stub_request(:get, %r{api/ident/v1/identification_requests})
+ stub_request(:get, %r{api/ident/v1/identification_requests/.+/proof_of_identity})
.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)
@@ -78,6 +97,233 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
assert_nil @contact.reload.verified_at
end
+ test 'should auto verify api user when sub is present' do
+ api_user = users(:api_bestnames_epp)
+ api_user.update!(email: 'api@example.test', ident_request_sent_at: 1.day.ago)
+ payload = {
+ identification_request_id: '456',
+ reference: api_user.uuid
+ }
+ signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
+
+ ident_body = {
+ id: '456',
+ status: 'completed',
+ result: {
+ sub: 'EE60001019906',
+ given_name: 'Test',
+ family_name: 'User'
+ }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/456})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ post '/eeid/webhooks/identification_requests', params: payload, as: :json,
+ headers: { 'X-HMAC-Signature' => signature }
+
+ assert_response :ok
+ api_user.reload
+ assert api_user.verified_at.present?
+ assert_equal 'EE', api_user.country_code
+ assert_equal 'EE60001019906', api_user.subject
+ assert_equal "eeid-webhook:ApiUser:#{api_user.username}", api_user.updator_str
+ assert_nil api_user.identity_code
+ assert_nil api_user.verification_pending_at
+ end
+
+ test 'should auto verify api user with document-only sub' do
+ api_user = users(:api_bestnames_epp)
+ api_user.update!(email: 'api@example.test', ident_request_sent_at: 1.day.ago)
+ payload = {
+ identification_request_id: '457',
+ reference: api_user.uuid
+ }
+ signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
+
+ ident_body = {
+ id: '457',
+ status: 'completed',
+ result: {
+ sub: 'GBAB123456',
+ given_name: 'Test',
+ family_name: 'User'
+ }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/457})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ post '/eeid/webhooks/identification_requests', params: payload, as: :json,
+ headers: { 'X-HMAC-Signature' => signature }
+
+ assert_response :ok
+ api_user.reload
+ assert api_user.verified_at.present?
+ assert_equal 'GBAB123456', api_user.subject
+ assert_nil api_user.identity_code
+ assert_nil api_user.verification_pending_at
+ end
+
+ test 'should pending review api user when subject cannot be resolved' do
+ api_user = users(:api_bestnames_epp)
+ api_user.update!(email: 'api@example.test', ident_request_sent_at: 1.day.ago)
+ payload = {
+ identification_request_id: '789',
+ reference: api_user.uuid
+ }
+ signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
+
+ ident_body = {
+ id: '789',
+ status: 'completed',
+ result: { given_name: 'Test', family_name: 'User', country: 'GB' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/789})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ post '/eeid/webhooks/identification_requests', params: payload, as: :json,
+ headers: { 'X-HMAC-Signature' => signature }
+
+ assert_response :ok
+ api_user.reload
+ assert_nil api_user.verified_at
+ assert api_user.verification_pending_at.present?
+ assert_equal 'User', api_user.verification_snapshot['family_name']
+ assert_nil api_user.subject
+ assert_emails 1
+ assert_includes ActionMailer::Base.deliveries.last.subject, api_user.username
+ end
+
+ test 'should pending review contact when sub mismatches contact ident' do
+ @contact.update!(ident_request_sent_at: Time.zone.now - 1.day)
+ request_id = '321'
+ payload = { identification_request_id: request_id, reference: @contact.code }
+ ident_body = {
+ id: request_id,
+ status: 'completed',
+ claims_matched: false,
+ result: { sub: 'US9999', given_name: 'John' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/#{request_id}$})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ post '/eeid/webhooks/identification_requests',
+ params: payload,
+ as: :json,
+ headers: { 'X-HMAC-Signature' => hmac_signature_for(payload) }
+
+ assert_response :ok
+ assert_equal({ 'status' => 'success' }, JSON.parse(response.body))
+
+ @contact.reload
+ assert_nil @contact.verified_at
+ assert @contact.verification_pending_at.present?
+ assert_equal request_id, @contact.verification_id
+ assert_equal 'US9999', @contact.verification_snapshot['sub']
+ assert_equal 'John', @contact.verification_snapshot['given_name']
+
+ assert_notify_registrar_pending_review(
+ "Teade: Kontakti [#{@contact.code}] isikutuvastus vajab registripidaja kinnitust " \
+ "/ Notification: Contact [#{@contact.code}] identity verification needs registrar review"
+ )
+ end
+
+ test 'contact verify to mismatch webhook ends in pending review with downloadable POI' do
+ user = users(:api_bestnames)
+ auth_headers = { 'Authorization' => repp_basic_auth(user) }
+
+ stub_request(:post, %r{api/ident/v1/identification_requests})
+ .with(
+ body: {
+ claims_required: [{ type: 'sub', value: 'US1234' }],
+ reference: @contact.code
+ }
+ ).to_return(
+ status: 200,
+ body: { id: '555', link: 'http://link' }.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ post "/repp/v1/contacts/verify/#{@contact.code}", headers: auth_headers
+
+ assert_response :ok
+ @contact.reload
+ assert @contact.ident_request_sent_at.present?
+ assert_nil @contact.verified_at
+ assert_nil @contact.verification_pending_at
+
+ request_id = '555'
+ payload = { identification_request_id: request_id, reference: @contact.code }
+ ident_body = {
+ id: request_id,
+ status: 'completed',
+ claims_matched: false,
+ result: { sub: 'US9999', given_name: 'John', family_name: 'Doe' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/#{request_id}$})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ ActionMailer::Base.deliveries.clear
+
+ post '/eeid/webhooks/identification_requests',
+ params: payload,
+ as: :json,
+ headers: { 'X-HMAC-Signature' => hmac_signature_for(payload) }
+
+ assert_response :ok
+ assert_equal({ 'status' => 'success' }, JSON.parse(response.body))
+
+ @contact.reload
+ assert_nil @contact.verified_at
+ assert @contact.verification_pending_at.present?
+ assert_equal request_id, @contact.verification_id
+ assert_equal 'US9999', @contact.verification_snapshot['sub']
+
+ assert_notify_registrar_pending_review(
+ "Teade: Kontakti [#{@contact.code}] isikutuvastus vajab registripidaja kinnitust " \
+ "/ Notification: Contact [#{@contact.code}] identity verification needs registrar review"
+ )
+
+ 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_#{request_id}.pdf\"; filename*=UTF-8''proof_of_identity_#{request_id}.pdf",
+ response.headers['Content-Disposition']
+ )
+ assert_not_empty response.body
+ end
+
+ test 'should pending review api user when subject conflicts with another user' do
+ existing = users(:api_bestnames)
+ existing.update!(subject: 'GBAB999999', registrar: users(:api_bestnames_epp).registrar)
+
+ api_user = users(:api_bestnames_epp)
+ api_user.update!(email: 'api@example.test', ident_request_sent_at: 1.day.ago)
+ payload = {
+ identification_request_id: '790',
+ reference: api_user.uuid
+ }
+ signature = OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
+
+ ident_body = {
+ id: '790',
+ status: 'completed',
+ result: { sub: 'GBAB999999' }
+ }.to_json
+ stub_request(:get, %r{api/ident/v1/identification_requests/790})
+ .to_return(status: 200, body: ident_body, headers: { 'Content-Type' => 'application/json' })
+
+ post '/eeid/webhooks/identification_requests', params: payload, as: :json,
+ headers: { 'X-HMAC-Signature' => signature }
+
+ assert_response :ok
+ api_user.reload
+ assert_nil api_user.verified_at
+ assert api_user.verification_pending_at.present?
+ assert_nil api_user.subject
+ end
+
test 'returns error response if throttled' do
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
@@ -93,6 +339,15 @@ class Eeid::IdentificationRequestsWebhookTest < ActionDispatch::IntegrationTest
private
+ def hmac_signature_for(payload)
+ OpenSSL::HMAC.hexdigest('SHA256', @secret, payload.to_json)
+ end
+
+ def repp_basic_auth(user)
+ token = Base64.encode64("#{user.username}:#{user.plain_text_password}")
+ "Basic #{token}"
+ end
+
def assert_notify_registrar(subject)
assert_emails 1
email = ActionMailer::Base.deliveries.last
@@ -101,4 +356,14 @@ def assert_notify_registrar(subject)
assert_equal 1, email.attachments.size
assert_equal 'proof_of_identity.pdf', email.attachments.first.filename
end
+
+ def assert_notify_registrar_pending_review(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
+ assert_not_empty email.attachments.first.body.raw_source
+ end
end
diff --git a/test/integration/repp/v1/accounts/switch_user_test.rb b/test/integration/repp/v1/accounts/switch_user_test.rb
index fa26b442d4..f10315c989 100644
--- a/test/integration/repp/v1/accounts/switch_user_test.rb
+++ b/test/integration/repp/v1/accounts/switch_user_test.rb
@@ -14,7 +14,7 @@ def setup
def test_switches_to_linked_api_user
new_user = users(:api_goodnames)
- new_user.update(identity_code: '1234')
+ new_user.update(subject: @user.subject)
request_body = {
account: {
new_user_id: new_user.id,
@@ -38,7 +38,7 @@ def test_switches_to_linked_api_user
def test_switches_to_unlinked_api_user
new_user = users(:api_goodnames)
- new_user.update(identity_code: '4444')
+ new_user.update(subject: 'EE44444444444')
request_body = {
account: {
new_user_id: new_user.id,
@@ -57,7 +57,7 @@ def test_returns_error_response_if_throttled
ENV['shunter_enabled'] = 'true'
new_user = users(:api_goodnames)
- new_user.update(identity_code: '1234')
+ new_user.update(subject: @user.subject)
request_body = {
account: {
new_user_id: new_user.id,
diff --git a/test/integration/repp/v1/api_users/create_test.rb b/test/integration/repp/v1/api_users/create_test.rb
index 8973507734..9518cf47d4 100644
--- a/test/integration/repp/v1/api_users/create_test.rb
+++ b/test/integration/repp/v1/api_users/create_test.rb
@@ -55,6 +55,24 @@ def test_validates_identity_code_per_registrar
assert json[:message].include? 'Identity code already exists'
end
+ def test_validates_subject_per_registrar
+ request_body = {
+ api_user: {
+ username: 'username',
+ plain_text_password: 'password',
+ active: true,
+ subject: @user.subject,
+ roles: ['super'],
+ },
+ }
+
+ post '/repp/v1/api_users', headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert json[:message].include? 'already used by another api user at this registrar'
+ end
+
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
diff --git a/test/integration/repp/v1/api_users/update_test.rb b/test/integration/repp/v1/api_users/update_test.rb
index 5c61856b4c..c3feadcb69 100644
--- a/test/integration/repp/v1/api_users/update_test.rb
+++ b/test/integration/repp/v1/api_users/update_test.rb
@@ -46,6 +46,21 @@ def test_can_not_change_identity_code_if_already_exists_per_registrar
assert json[:message].include? 'Identity code already exists'
end
+ def test_can_not_change_subject_if_already_exists_per_registrar
+ epp_user = users(:api_bestnames_epp)
+ request_body = {
+ api_user: {
+ subject: @user.subject,
+ },
+ }
+
+ put "/repp/v1/api_users/#{epp_user.id}", headers: @auth_headers, params: request_body
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :bad_request
+ assert json[:message].include? 'already used by another api user at this registrar'
+ end
+
def test_returns_error_if_password_wrong_format
request_body = {
api_user: {
diff --git a/test/integration/repp/v1/api_users/verify_test.rb b/test/integration/repp/v1/api_users/verify_test.rb
new file mode 100644
index 0000000000..aa3c129ccb
--- /dev/null
+++ b/test/integration/repp/v1/api_users/verify_test.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class ReppV1ApiUsersVerifyTest < ActionDispatch::IntegrationTest
+ def setup
+ @api_user = users(:api_bestnames_epp)
+ @api_user.update!(email: 'verify@example.test', subject: nil)
+ @user = users(:api_bestnames)
+ token = Base64.encode64("#{@user.username}:#{@user.plain_text_password}")
+ @auth_headers = { 'Authorization' => "Basic #{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: {}
+ )
+ stub_request(:post, %r{api/ident/v1/identification_requests})
+ .with(
+ body: hash_including(
+ 'claims_required' => [{ 'type' => 'sub', 'value' => '' }],
+ 'reference' => @api_user.uuid
+ )
+ ).to_return(status: 200, body: { id: '123', link: 'http://link' }.to_json,
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def test_verifies_api_user_with_discovery_claims
+ post "/repp/v1/api_users/verify/#{@api_user.id}", headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+
+ @api_user.reload
+ assert @api_user.ident_request_sent_at.present?
+ assert_nil @api_user.verified_at
+ assert_emails 1
+ assert_equal ['verify@example.test'], ActionMailer::Base.deliveries.last.to
+ end
+
+ def test_returns_error_without_email
+ @api_user.update!(email: nil)
+
+ post "/repp/v1/api_users/verify/#{@api_user.id}", headers: @auth_headers
+
+ assert_response :bad_request
+ assert_nil @api_user.reload.ident_request_sent_at
+ assert_emails 0
+ end
+
+ def test_verifies_api_user_with_existing_subject
+ @api_user.update!(subject: 'EE60001019906', country_code: 'EE')
+ stub_request(:post, %r{api/ident/v1/identification_requests})
+ .with(
+ body: hash_including(
+ 'claims_required' => [{ 'type' => 'sub', 'value' => 'EE60001019906' }]
+ )
+ ).to_return(status: 200, body: { id: '123', link: 'http://link' }.to_json,
+ headers: { 'Content-Type' => 'application/json' })
+
+ post "/repp/v1/api_users/verify/#{@api_user.id}", headers: @auth_headers
+
+ assert_response :ok
+ assert @api_user.reload.ident_request_sent_at.present?
+ end
+end
diff --git a/test/integration/repp/v1/contacts/download_poi_test.rb b/test/integration/repp/v1/contacts/download_poi_test.rb
index 1d943dc5c6..3c91aba150 100644
--- a/test/integration/repp/v1/contacts/download_poi_test.rb
+++ b/test/integration/repp/v1/contacts/download_poi_test.rb
@@ -41,6 +41,21 @@ def test_downloads_poi_for_contact
assert_not_empty response.body
end
+ def test_downloads_poi_for_contact_pending_registrar_review
+ @contact.update!(
+ ident_request_sent_at: 1.day.ago,
+ verification_pending_at: Time.zone.now,
+ verification_id: '321',
+ verification_snapshot: { sub: 'US9999', given_name: 'John' }
+ )
+ 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_321.pdf\"; filename*=UTF-8''proof_of_identity_321.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(
diff --git a/test/integration/repp/v1/domains/update_test.rb b/test/integration/repp/v1/domains/update_test.rb
index 1026110e6a..c5dfd9c995 100644
--- a/test/integration/repp/v1/domains/update_test.rb
+++ b/test/integration/repp/v1/domains/update_test.rb
@@ -51,7 +51,7 @@ def test_adds_epp_error_when_disputed_domain_updated_without_registrant_change
json,
http_status: :bad_request,
code: 2304,
- message: 'Object status prohibits operation; disputed domain update requires registrant change'
+ message: I18n.t('activerecord.errors.models.epp_domain.attributes.base.dispute_update_requires_registrant_change')
)
assert_equal old_auth_code, @domain.auth_info
assert @domain.disputed?
@@ -67,7 +67,7 @@ def test_adds_epp_error_when_reserved_pw_is_missing_for_disputed_registrant_chan
json,
http_status: :bad_request,
code: 2304,
- message: 'Required parameter missing; reservedpw element required for dispute domains'
+ message: I18n.t('activerecord.errors.models.epp_domain.attributes.base.required_parameter_missing_disputed')
)
refute_equal new_registrant.code, @domain.registrant.code
assert @domain.disputed?
@@ -83,7 +83,7 @@ def test_rejects_disputed_domain_update_with_valid_pw_but_no_registrant_change
json,
http_status: :bad_request,
code: 2304,
- message: 'Object status prohibits operation; disputed domain update requires registrant change'
+ message: I18n.t('activerecord.errors.models.epp_domain.attributes.base.dispute_update_requires_registrant_change')
)
assert_equal old_auth_code, @domain.auth_info
assert @domain.disputed?
@@ -141,7 +141,7 @@ def test_adds_epp_error_when_reserved_pw_is_invalid_for_disputed_domain
json,
http_status: :bad_request,
code: 2202,
- message: 'Invalid authorization information; invalid reserved>pw value'
+ message: I18n.t('activerecord.errors.models.epp_domain.attributes.base.invalid_auth_information_disputed')
)
refute_equal new_registrant.code, @domain.registrant.code
assert @domain.disputed?
@@ -161,7 +161,7 @@ def test_rejects_disputed_domain_registrant_and_transfer_code_change_without_res
json,
http_status: :bad_request,
code: 2304,
- message: 'Required parameter missing; reservedpw element required for dispute domains'
+ message: I18n.t('activerecord.errors.models.epp_domain.attributes.base.required_parameter_missing_disputed')
)
refute_equal new_registrant.code, @domain.registrant.code
assert_equal old_transfer_code, @domain.transfer_code
@@ -197,6 +197,17 @@ def update_domain(domain_params)
JSON.parse(response.body, symbolize_names: true)
end
+ def assert_repp_success(json)
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ end
+
+ def assert_repp_error(json, http_status:, code:, message:)
+ assert_response http_status
+ assert_equal code, json[:code]
+ assert_equal message, json[:message]
+ end
+
def json_headers
@auth_headers.merge('Content-Type' => 'application/json')
end
@@ -209,16 +220,4 @@ def create_dispute(password: '1234567890')
expires_at: Time.zone.now + 5.days
)
end
-
- def assert_repp_success(json)
- assert_response :ok
- assert_equal 1000, json[:code]
- assert_equal 'Command completed successfully', json[:message]
- end
-
- def assert_repp_error(json, http_status:, code:, message:)
- assert_response http_status
- assert_equal code, json[:code]
- assert_equal message, json[:message]
- end
end
diff --git a/test/integration/repp/v1/registrar/auth/check_info_test.rb b/test/integration/repp/v1/registrar/auth/check_info_test.rb
index 8fd321db18..770174231d 100644
--- a/test/integration/repp/v1/registrar/auth/check_info_test.rb
+++ b/test/integration/repp/v1/registrar/auth/check_info_test.rb
@@ -39,6 +39,17 @@ def test_invalid_user_login
assert_equal json[:message], 'Invalid authorization information'
end
+ def test_rejects_unverified_user_login_with_valid_password
+ @user.update_columns(verified_at: nil)
+
+ get '/repp/v1/registrar/auth', headers: @auth_headers
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :unauthorized
+ assert_equal I18n.t('registrar.authorization.identity_not_verified'), json[:message]
+ assert_equal false, json[:data][:eligible_for_sign_in]
+ end
+
def test_returns_error_response_if_throttled
ENV['shunter_default_threshold'] = '1'
ENV['shunter_enabled'] = 'true'
diff --git a/test/integration/repp/v1/registrar/auth/tara_callback_test.rb b/test/integration/repp/v1/registrar/auth/tara_callback_test.rb
index e9c1a101c5..56ae6a5380 100644
--- a/test/integration/repp/v1/registrar/auth/tara_callback_test.rb
+++ b/test/integration/repp/v1/registrar/auth/tara_callback_test.rb
@@ -35,6 +35,26 @@ def test_validates_user_from_omniauth_params
assert_equal json[:data][:token], user_token
end
+ def test_auto_verifies_unverified_api_user_with_matching_subject
+ @user.update_columns(verified_at: nil)
+
+ request_body = { auth: { uid: 'EE1234' } }
+
+ Repp::V1::BaseController.stub_any_instance(:webclient_request?, true) do
+ Repp::V1::BaseController.stub_any_instance(:validate_webclient, true) do
+ post '/repp/v1/registrar/auth/tara_callback', headers: @auth_headers, params: request_body
+ end
+ end
+
+ json = JSON.parse(response.body, symbolize_names: true)
+
+ assert_response :ok
+ assert_equal 1000, json[:code]
+ @user.reload
+ assert @user.verified_at.present?
+ assert_equal "eeid-auto-verify:#{@user.username}", @user.updator_str
+ end
+
def test_invalidates_user_with_wrong_omniauth_params
request_body = {
auth: {
diff --git a/test/interactions/actions/api_user_approve_verification_test.rb b/test/interactions/actions/api_user_approve_verification_test.rb
new file mode 100644
index 0000000000..499b21a886
--- /dev/null
+++ b/test/interactions/actions/api_user_approve_verification_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Actions::ApiUserApproveVerificationTest < ActiveSupport::TestCase
+ setup do
+ @api_user = users(:api_bestnames_epp)
+ @api_user.update!(
+ email: 'pending@example.test',
+ ident_request_sent_at: 1.day.ago,
+ verification_pending_at: Time.zone.now,
+ verification_id: 'pending-1',
+ verification_snapshot: {
+ 'sub' => 'GBMANUAL123'
+ }
+ )
+ end
+
+ test 'approve sets subject and country from verification snapshot' do
+ assert Actions::ApiUserApproveVerification.new(@api_user).call
+
+ @api_user.reload
+ assert @api_user.verified_at.present?
+ assert_nil @api_user.verification_pending_at
+ assert_equal 'GBMANUAL123', @api_user.subject
+ assert_equal 'GB', @api_user.country_code
+ assert_nil @api_user.identity_code
+ end
+
+ test 'approve sets subject from manual override when snapshot has no subject' do
+ @api_user.update!(verification_snapshot: { 'given_name' => 'Test' })
+
+ assert Actions::ApiUserApproveVerification.new(@api_user, subject: 'EE60001019906').call
+
+ @api_user.reload
+ assert_equal 'EE60001019906', @api_user.subject
+ assert_equal 'EE', @api_user.country_code
+ assert_nil @api_user.identity_code
+ end
+
+ test 'approve fails when subject is missing' do
+ @api_user.update!(verification_snapshot: { 'given_name' => 'Test' })
+
+ assert_not Actions::ApiUserApproveVerification.new(@api_user).call
+ assert @api_user.errors.added?(:base, :missing_subject)
+ end
+
+ test 'approve fails when subject conflicts' do
+ users(:api_bestnames).update!(
+ subject: 'GBMANUAL123',
+ registrar: @api_user.registrar
+ )
+
+ assert_not Actions::ApiUserApproveVerification.new(@api_user).call
+ assert @api_user.errors.added?(:subject, :taken)
+ end
+end
diff --git a/test/interactions/actions/api_user_reject_verification_test.rb b/test/interactions/actions/api_user_reject_verification_test.rb
new file mode 100644
index 0000000000..5fa9fd3149
--- /dev/null
+++ b/test/interactions/actions/api_user_reject_verification_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Actions::ApiUserRejectVerificationTest < ActiveSupport::TestCase
+ setup do
+ @api_user = users(:api_bestnames_epp)
+ @api_user.update!(
+ email: 'pending@example.test',
+ ident_request_sent_at: 1.day.ago,
+ verification_pending_at: Time.zone.now,
+ verification_id: 'pending-1',
+ verification_snapshot: { 'sub' => 'GBMANUAL123' }
+ )
+ end
+
+ test 'reject clears pending verification and ident request timestamp' do
+ assert Actions::ApiUserRejectVerification.new(@api_user).call
+
+ @api_user.reload
+ assert_nil @api_user.ident_request_sent_at
+ assert_nil @api_user.verification_pending_at
+ assert_nil @api_user.verification_id
+ assert_equal({}, @api_user.verification_snapshot)
+ assert_nil @api_user.verified_at
+ end
+
+ test 'reject fails when not pending verification' do
+ @api_user.update!(verification_pending_at: nil)
+
+ assert_not Actions::ApiUserRejectVerification.new(@api_user).call
+ assert @api_user.errors.added?(:base, :not_pending_verification)
+ end
+end
diff --git a/test/interactions/actions/contact_approve_verification_test.rb b/test/interactions/actions/contact_approve_verification_test.rb
new file mode 100644
index 0000000000..29f59e7b83
--- /dev/null
+++ b/test/interactions/actions/contact_approve_verification_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Actions::ContactApproveVerificationTest < ActiveSupport::TestCase
+ setup do
+ @contact = contacts(:john)
+ @contact.update!(
+ ident_request_sent_at: 1.day.ago,
+ verification_pending_at: Time.zone.now,
+ verification_id: 'pending-1',
+ verification_snapshot: {
+ 'sub' => 'US9999'
+ }
+ )
+ end
+
+ test 'approve sets ident from verification snapshot' do
+ assert Actions::ContactApproveVerification.new(@contact).call
+
+ @contact.reload
+ assert @contact.verified_at.present?
+ assert_nil @contact.verification_pending_at
+ assert_equal 'US', @contact.ident_country_code
+ assert_equal '9999', @contact.ident
+ end
+
+ test 'approve fails when subject is missing' do
+ @contact.update!(verification_snapshot: { 'given_name' => 'Test' })
+
+ assert_not Actions::ContactApproveVerification.new(@contact).call
+ assert @contact.errors.added?(:base, :missing_subject)
+ end
+
+ test 'approve fails for birthday contact when snapshot contains id_number' do
+ @contact.update!(
+ ident_type: Contact::BIRTHDAY,
+ ident: '2010-07-05',
+ ident_country_code: 'EE',
+ verification_snapshot: {
+ 'date_of_birth' => '2010-07-05',
+ 'given_name' => 'Child',
+ 'family_name' => 'Example',
+ 'country' => 'EE',
+ 'id_number' => '30303039914'
+ }
+ )
+
+ assert_not Actions::ContactApproveVerification.new(@contact).call
+ assert @contact.errors.added?(:base, :id_number_requires_priv_contact)
+ assert_nil @contact.reload.verified_at
+ end
+
+ test 'approve succeeds for birthday contact without id_number in snapshot' do
+ @contact.update!(
+ ident_type: Contact::BIRTHDAY,
+ ident: '2010-07-05',
+ ident_country_code: 'EE',
+ name: 'Child Example',
+ verification_snapshot: {
+ 'date_of_birth' => '2010-07-05',
+ 'given_name' => 'Child',
+ 'family_name' => 'Example',
+ 'country' => 'EE',
+ 'document_number' => 'AB123456'
+ }
+ )
+
+ assert Actions::ContactApproveVerification.new(@contact).call
+
+ @contact.reload
+ assert @contact.verified_at.present?
+ assert_equal '2010-07-05', @contact.ident
+ assert_equal 'EE', @contact.ident_country_code
+ assert_equal 'Child Example', @contact.name
+ end
+end
diff --git a/test/interactions/actions/contact_reject_verification_test.rb b/test/interactions/actions/contact_reject_verification_test.rb
new file mode 100644
index 0000000000..bd32fe870a
--- /dev/null
+++ b/test/interactions/actions/contact_reject_verification_test.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Actions::ContactRejectVerificationTest < ActiveSupport::TestCase
+ setup do
+ @contact = contacts(:john)
+ @contact.update!(
+ ident_request_sent_at: 1.day.ago,
+ verification_pending_at: Time.zone.now,
+ verification_id: 'pending-1',
+ verification_snapshot: { 'sub' => 'US9999' }
+ )
+ end
+
+ test 'reject clears pending verification and ident request timestamp' do
+ assert Actions::ContactRejectVerification.new(@contact).call
+
+ @contact.reload
+ assert_nil @contact.ident_request_sent_at
+ assert_nil @contact.verification_pending_at
+ assert_nil @contact.verification_id
+ assert_equal({}, @contact.verification_snapshot)
+ assert_nil @contact.verified_at
+ end
+
+ test 'reject fails when not pending verification' do
+ @contact.update!(verification_pending_at: nil)
+
+ assert_not Actions::ContactRejectVerification.new(@contact).call
+ assert @contact.errors.added?(:base, :not_pending_verification)
+ end
+end
diff --git a/test/interactions/actions/process_contact_identification_webhook_test.rb b/test/interactions/actions/process_contact_identification_webhook_test.rb
new file mode 100644
index 0000000000..19f29496b1
--- /dev/null
+++ b/test/interactions/actions/process_contact_identification_webhook_test.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class Actions::ProcessContactIdentificationWebhookTest < ActiveSupport::TestCase
+ setup do
+ @contact = contacts(:john)
+ @contact.update!(ident_request_sent_at: 1.day.ago)
+ end
+
+ test 'auto verifies when sub matches contact ident' do
+ action = Actions::ProcessContactIdentificationWebhook.new(
+ @contact,
+ identification_request_id: '123',
+ result: { sub: 'US1234' }
+ )
+
+ assert action.call
+ assert_equal :auto_verified, action.outcome
+
+ @contact.reload
+ assert @contact.verified_at.present?
+ assert_nil @contact.verification_pending_at
+ assert_equal '123', @contact.verification_id
+ end
+
+ test 'pending review when sub mismatches contact ident' do
+ action = Actions::ProcessContactIdentificationWebhook.new(
+ @contact,
+ identification_request_id: '124',
+ result: { sub: 'US9999', given_name: 'John' }
+ )
+
+ assert action.call
+ assert_equal :pending_review, action.outcome
+
+ @contact.reload
+ assert_nil @contact.verified_at
+ assert @contact.verification_pending_at.present?
+ assert_equal 'US9999', @contact.verification_snapshot['sub']
+ end
+
+ test 'pending review for birthday contact when result contains id_number' do
+ @contact.update!(
+ ident_type: Contact::BIRTHDAY,
+ ident: '2010-07-05',
+ ident_country_code: 'EE',
+ name: 'Child Example'
+ )
+
+ action = Actions::ProcessContactIdentificationWebhook.new(
+ @contact,
+ identification_request_id: '125',
+ result: {
+ date_of_birth: '2010-07-05',
+ given_name: 'Child',
+ family_name: 'Example',
+ country: 'EE',
+ id_number: '30303039914'
+ }
+ )
+
+ assert action.call
+ assert_equal :pending_review, action.outcome
+
+ @contact.reload
+ assert @contact.verification_pending_at.present?
+ assert_equal '30303039914', @contact.verification_snapshot['id_number']
+ end
+end
diff --git a/test/models/api_user_test.rb b/test/models/api_user_test.rb
index ec53d2371e..a424debec5 100644
--- a/test/models/api_user_test.rb
+++ b/test/models/api_user_test.rb
@@ -1,8 +1,12 @@
require 'test_helper'
class ApiUserTest < ActiveSupport::TestCase
+ include ActionMailer::TestHelper
+
setup do
@user = users(:api_bestnames)
+ @user.reload
+ ActionMailer::Base.deliveries.clear
end
def test_valid_user_fixture_is_valid
@@ -17,18 +21,20 @@ def test_invalid_without_username
def test_invalid_when_username_is_already_taken
user = valid_user
- another_user = user.dup
+ another_user = duplicate_as_new_record(user)
assert another_user.invalid?
- another_user.username = 'another'
- another_user.identity_code = ''
- assert another_user.valid?
+ another_user.username = 'another_username_not_taken'
+ another_user.identity_code = nil
+ another_user.subject = nil
+ another_user.registrar = user.registrar
+ assert another_user.valid?, another_user.errors.full_messages
end
def test_invalid_when_one_registrar_and_identity_code_is_already_taken
user = valid_user
- another_user = user.dup
+ another_user = duplicate_as_new_record(user)
assert another_user.invalid?
@@ -69,6 +75,164 @@ def test_active_by_default
assert ApiUser.new.active?
end
+ def test_invalid_when_subject_already_used_at_registrar
+ login_subject = 'EE1234'
+ @user.update_columns(subject: login_subject)
+
+ another_user = ApiUser.new(
+ username: 'another_subject_user',
+ plain_text_password: 'secret1',
+ registrar_id: @user.registrar_id,
+ roles: ['epp'],
+ subject: login_subject
+ )
+
+ assert another_user.invalid?, another_user.errors.full_messages
+ assert_match(
+ /already used by another api user at this registrar/i,
+ another_user.errors[:subject].join
+ )
+ end
+
+ def test_linked_users_by_subject_across_registrars
+ login_subject = 'GBAB123456'
+ @user.update_columns(subject: login_subject)
+ linked = ApiUser.create!(
+ username: 'linked_other_registrar',
+ plain_text_password: 'secret1',
+ registrar: registrars(:goodnames),
+ roles: ['epp'],
+ subject: login_subject,
+ verified_at: Time.zone.now,
+ active: true
+ )
+
+ assert_includes @user.linked_users, linked
+ end
+
+ def test_linked_users_empty_when_subject_blank
+ @user.update_columns(subject: nil)
+
+ assert_empty @user.linked_users
+ end
+
+ def test_linked_users_excludes_inactive_users
+ login_subject = 'EE99999999999'
+ @user.update_columns(subject: login_subject)
+ linked = ApiUser.create!(
+ username: 'linked_inactive',
+ plain_text_password: 'secret1',
+ registrar: registrars(:goodnames),
+ roles: ['epp'],
+ subject: login_subject,
+ verified_at: Time.zone.now
+ )
+ linked.update_columns(active: false)
+
+ assert_empty @user.linked_users
+ end
+
+ def test_linked_users_excludes_unverified_users
+ login_subject = 'EE77777777777'
+ @user.update_columns(subject: login_subject)
+ ApiUser.create!(
+ username: 'linked_unverified',
+ plain_text_password: 'secret1',
+ registrar: registrars(:goodnames),
+ roles: ['epp'],
+ subject: login_subject,
+ verified_at: nil,
+ active: true
+ )
+
+ assert_empty @user.linked_users
+ end
+
+ def test_linked_with_by_subject_only
+ @user.update_columns(subject: 'GBAB1')
+ same_subject = ApiUser.new(subject: 'GBAB1')
+ different_subject = ApiUser.new(subject: 'other')
+
+ assert @user.linked_with?(same_subject)
+ assert_not @user.linked_with?(different_subject)
+ assert_not @user.linked_with?(ApiUser.new(subject: nil))
+ assert_not @user.linked_with?(nil)
+ end
+
+ def test_eligible_for_sign_in_requires_active_and_verified
+ @user.update_columns(active: true, verified_at: Time.zone.now)
+ assert @user.eligible_for_sign_in?
+
+ @user.update_columns(active: false)
+ assert_not @user.eligible_for_sign_in?
+
+ @user.update_columns(active: true, subject: 'EE1234', verified_at: nil)
+ assert_not @user.eligible_for_sign_in?
+ end
+
+ def test_subject_change_clears_verification_status_when_subject_previously_present
+ @user.update_columns(
+ subject: 'EE1234',
+ ident_request_sent_at: 2.days.ago,
+ verified_at: 1.day.ago,
+ verification_id: 'ver-1',
+ verification_pending_at: 3.hours.ago,
+ verification_snapshot: { 'sub' => 'EE1234' }
+ )
+
+ @user.update!(subject: 'EE9999')
+ @user.reload
+
+ assert_nil @user.ident_request_sent_at
+ assert_nil @user.verified_at
+ assert_nil @user.verification_id
+ assert_nil @user.verification_pending_at
+ assert_equal({}, @user.verification_snapshot)
+ end
+
+ def test_subject_change_notifies_registrar_when_subject_previously_present
+ @user.update_columns(
+ subject: 'EE1234',
+ verified_at: 1.day.ago
+ )
+
+ assert_emails 1 do
+ @user.update!(subject: 'EE9999')
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ assert_equal [@user.registrar.email], email.to
+ assert_match(@user.username, email.subject)
+ end
+
+ def test_setting_subject_first_time_does_not_clear_verification_status
+ @user.update_columns(
+ subject: nil,
+ ident_request_sent_at: 2.days.ago,
+ verified_at: 1.day.ago,
+ verification_id: 'ver-2',
+ verification_pending_at: nil,
+ verification_snapshot: { 'sub' => 'EE1234' }
+ )
+
+ @user.update!(subject: 'EE1234')
+ @user.reload
+
+ assert_not_nil @user.ident_request_sent_at
+ assert_not_nil @user.verified_at
+ assert_equal 'ver-2', @user.verification_id
+ assert_nil @user.verification_pending_at
+ assert_equal({ 'sub' => 'EE1234' }, @user.verification_snapshot)
+ end
+
+ def test_setting_subject_first_time_does_not_notify_registrar
+ @user.update_columns(subject: nil)
+
+ assert_emails 0 do
+ @user.update!(subject: 'EE1234')
+ end
+ end
+
def test_verifies_pki_status
certificate = certificates(:api)
@@ -93,4 +257,10 @@ def test_verifies_pki_status
def valid_user
users(:api_bestnames)
end
+
+ def duplicate_as_new_record(user)
+ ApiUser.new(
+ user.attributes.except('id', 'created_at', 'updated_at', 'uuid', 'subject')
+ )
+ end
end
diff --git a/test/models/domain_test.rb b/test/models/domain_test.rb
index e94225343f..c08a2d5d2a 100644
--- a/test/models/domain_test.rb
+++ b/test/models/domain_test.rb
@@ -129,8 +129,8 @@ def test_invalid_when_domain_is_reserved
domain.name = reserved_domain.name
assert domain.invalid?
- assert_includes domain.errors.full_messages, 'Required parameter missing; reserved>' \
- 'pw element required for reserved domains'
+ assert_includes domain.errors.full_messages,
+ I18n.t('activerecord.errors.models.domain.attributes.base.required_parameter_missing_reserved')
end
def test_invalid_without_registration_period
@@ -537,7 +537,7 @@ def test_validates_admin_contact_required_for_legal_entity_registrant
domain.admin_domain_contacts.clear
assert domain.invalid?
- assert_includes domain.errors.full_messages, 'Admin domain contacts Admin contacts count must be between 1-10'
+ assert_includes domain.errors.full_messages, admin_contacts_count_out_of_range_message
domain.admin_domain_contacts.build(contact: contacts(:john))
assert domain.valid?
@@ -585,7 +585,7 @@ def test_validates_admin_contact_required_for_underage_estonian_id
domain.admin_domain_contacts.clear
assert domain.invalid?
- assert_includes domain.errors.full_messages, 'Admin domain contacts Admin contacts count must be between 1-10'
+ assert_includes domain.errors.full_messages, admin_contacts_count_out_of_range_message
admin_contact = contacts(:john)
admin_contact.update!(
@@ -637,8 +637,7 @@ def test_validates_admin_contact_required_for_org_based_on_setting
Setting.admin_contacts_required_for_org = true
domain.admin_domain_contacts.clear
assert domain.invalid?
- assert_includes domain.errors.full_messages,
- 'Admin domain contacts Admin contacts count must be between 1-10'
+ assert_includes domain.errors.full_messages, admin_contacts_count_out_of_range_message
# When setting is false
Setting.admin_contacts_required_for_org = false
@@ -655,8 +654,7 @@ def test_validates_admin_contact_required_for_minors_based_on_setting
Setting.admin_contacts_required_for_minors = true
domain.admin_domain_contacts.clear
assert domain.invalid?
- assert_includes domain.errors.full_messages,
- 'Admin domain contacts Admin contacts count must be between 1-10'
+ assert_includes domain.errors.full_messages, admin_contacts_count_out_of_range_message
# When setting is false
Setting.admin_contacts_required_for_minors = false
@@ -666,6 +664,14 @@ def test_validates_admin_contact_required_for_minors_based_on_setting
private
+ def admin_contacts_count_out_of_range_message
+ min = Setting.admin_contacts_min_count
+ max = Setting.admin_contacts_max_count
+ Domain.new.tap do |domain|
+ domain.errors.add(:admin_domain_contacts, :out_of_range, min: min, max: max)
+ end.errors.full_messages.first
+ end
+
def valid_domain
domains(:shop)
end
diff --git a/test/models/invoice_state_machinte_test.rb b/test/models/invoice_state_machinte_test.rb
index 319da1aa2d..f3989f13ff 100644
--- a/test/models/invoice_state_machinte_test.rb
+++ b/test/models/invoice_state_machinte_test.rb
@@ -45,8 +45,8 @@ def test_only_unpaid_invoice_can_be_cancelled
InvoiceStateMachine.new(invoice: @invoice, status: 'cancelled').call
@invoice.reload
- assert_equal @invoice.errors.full_messages.join, 'Inavalid state cancelled'
assert @invoice.errors.present?
+ assert_equal invalid_state_message('cancelled'), @invoice.errors.full_messages.join
end
def test_cancelled_invoiced_cannot_be_unpaid
@@ -63,7 +63,7 @@ def test_cancelled_invoiced_cannot_be_unpaid
assert @unpaid.cancelled?
assert @unpaid.errors.present?
- assert_equal @unpaid.errors.full_messages.join, 'Inavalid state unpaid'
+ assert_equal invalid_state_message('unpaid'), @unpaid.errors.full_messages.join
end
def test_if_paid_invoice_not_have_response_from_everypay_it_can_be_unpaid_back
@@ -100,6 +100,12 @@ def test_if_paid_invoice_has_response_from_everypay_it_cannot_be_rollback
assert @unpaid.paid?
assert @unpaid.errors.present?
- assert_equal @unpaid.errors.full_messages.join, 'Inavalid state unpaid'
+ assert_equal invalid_state_message('unpaid'), @unpaid.errors.full_messages.join
+ end
+
+ private
+
+ def invalid_state_message(status)
+ I18n.t('activerecord.errors.models.invoice.attributes.base.invalid_state', status: status)
end
end
diff --git a/test/models/user/from_omniauth_test.rb b/test/models/user/from_omniauth_test.rb
new file mode 100644
index 0000000000..7ec0548223
--- /dev/null
+++ b/test/models/user/from_omniauth_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class UserFromOmniauthTest < ActiveSupport::TestCase
+ setup do
+ @user = users(:api_bestnames)
+ end
+
+ test 'finds active api user by subject when uid matches' do
+ @user.update_columns(subject: 'EE60001019906', verified_at: nil, active: true)
+
+ found = ApiUser.from_omniauth('uid' => 'EE60001019906')
+
+ assert_equal @user, found
+ end
+
+ test 'does not match when subject is blank' do
+ @user.update_columns(subject: nil, verified_at: Time.zone.now, active: true)
+
+ assert_nil ApiUser.from_omniauth('uid' => 'EE60001019906')
+ end
+
+ test 'does not match by identity_code when subject is blank' do
+ @user.update_columns(subject: nil, identity_code: '1234')
+
+ assert_nil ApiUser.from_omniauth('uid' => 'EE1234')
+ end
+
+ test 'returns nil when uid is blank' do
+ assert_nil ApiUser.from_omniauth('uid' => '')
+ assert_nil ApiUser.from_omniauth({})
+ end
+
+ test 'returns nil when no api user matches subject' do
+ assert_nil ApiUser.from_omniauth('uid' => 'XXunknown')
+ end
+
+ test 'does not match inactive api users' do
+ @user.update_columns(subject: 'EE1234', active: false, verified_at: Time.zone.now)
+
+ assert_nil ApiUser.from_omniauth('uid' => 'EE1234')
+ end
+end
diff --git a/test/system/admin_area/registrars/api_users_test.rb b/test/system/admin_area/registrars/api_users_test.rb
index 2cd927b707..d2d0db7be1 100644
--- a/test/system/admin_area/registrars/api_users_test.rb
+++ b/test/system/admin_area/registrars/api_users_test.rb
@@ -51,7 +51,7 @@ def test_shows_api_user_details
assert_text "Username #{api_user.username}"
assert_text "Password #{api_user.plain_text_password}"
assert_link api_user.registrar.name, href: admin_registrar_path(api_user.registrar)
- assert_text "Role #{api_user.roles.first}"
+ assert_text "#{ApiUser.human_attribute_name(:roles)} #{api_user.roles.join(', ')}"
assert_text "Active #{api_user.active}"
end
@@ -83,11 +83,16 @@ def test_deletes_api_user
private
def unassociated_api_user
- new_api_user = users(:api_bestnames).dup
- new_api_user.username = "unique-#{rand(100)}"
- new_api_user.identity_code = rand(10)
- new_api_user.save!
- new_api_user
+ source = users(:api_bestnames)
+ ApiUser.create!(
+ username: "unique-#{SecureRandom.hex(4)}",
+ plain_text_password: source.plain_text_password,
+ identity_code: SecureRandom.random_number(10_000_000),
+ registrar: source.registrar,
+ roles: source.roles,
+ active: true,
+ subject: nil
+ )
end
def valid_password