Merge pull request #1574 from internetee/269-dispute-list

Implement disputed logic for domains
This commit is contained in:
Timo Võhmar 2020-05-22 16:53:58 +03:00 committed by GitHub
commit 9308ed7b91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1073 additions and 76 deletions

View file

@ -0,0 +1,74 @@
# frozen_string_literal: true
module Admin
class DisputesController < BaseController
load_and_authorize_resource
before_action :set_dispute, only: %i[show edit update delete]
# GET /admin/disputes
def index
params[:q] ||= {}
@disputes = sortable_dispute_query_for(Dispute.active.all, params[:q])
@closed_disputes = sortable_dispute_query_for(Dispute.closed.all, params[:q], closed: true)
end
# GET /admin/disputes/1
def show; end
# GET /admin/disputes/new
def new
@dispute = Dispute.new
end
# GET /admin/disputes/1/edit
def edit; end
# POST /admin/disputes
def create
@dispute = Dispute.new(dispute_params)
if @dispute.save
notice = 'Dispute was successfully created'
notice += @dispute.domain ? '.' : ' for domain that is not registered.'
redirect_to admin_disputes_url, notice: notice
else
render :new
end
end
# PATCH/PUT /admin/disputes/1
def update
if @dispute.update(dispute_params.except(:domain_name))
redirect_to admin_disputes_url, notice: 'Dispute was successfully updated.'
else
render :edit
end
end
# DELETE /admin/disputes/1
def delete
@dispute.close(initiator: 'Admin')
redirect_to admin_disputes_url, notice: 'Dispute was successfully closed.'
end
private
def sortable_dispute_query_for(disputes, query, closed: false)
@q = disputes.order(:domain_name).search(query)
disputes = @q.result.page(closed ? params[:closed_page] : params[:page])
return disputes.per(params[:results_per_page]) if params[:results_per_page].present?
disputes
end
# Use callbacks to share common setup or constraints between actions.
def set_dispute
@dispute = Dispute.find(params[:id])
end
# Only allow a trusted parameter "white list" through.
def dispute_params
params.require(:dispute).permit(:domain_name, :password, :starts_at, :comment)
end
end
end

View file

@ -92,7 +92,7 @@ module Epp
status: Auction.statuses[:payment_received])
active_auction.domain_registered!
end
Dispute.close_by_domain(@domain.name)
render_epp_response '/epp/domains/create'
else
handle_errors(@domain)
@ -103,21 +103,17 @@ module Epp
def update
authorize! :update, @domain, @password
if @domain.update(params[:parsed_frame], current_user)
if @domain.epp_pending_update.present?
render_epp_response '/epp/domains/success_pending'
else
render_epp_response '/epp/domains/success'
end
else
handle_errors(@domain)
end
updated = @domain.update(params[:parsed_frame], current_user)
(handle_errors(@domain) && return) unless updated
pending = @domain.epp_pending_update.present?
render_epp_response "/epp/domains/success#{'_pending' if pending}"
end
def delete
authorize! :delete, @domain, @password
handle_errors(@domain) and return unless @domain.can_be_deleted?
(handle_errors(@domain) && return) unless @domain.can_be_deleted?
if @domain.epp_destroy(params[:parsed_frame], current_user.id)
if @domain.epp_pending_delete.present?

View file

@ -4,6 +4,7 @@ class Registrant::DomainDeleteConfirmsController < RegistrantController
def show
return if params[:confirmed] || params[:rejected]
@domain = Domain.find(params[:id])
@domain = nil unless @domain.registrant_delete_confirmable?(params[:token])
end
@ -21,22 +22,23 @@ class Registrant::DomainDeleteConfirmsController < RegistrantController
initiator = current_registrant_user ? current_registrant_user.username :
t(:user_not_authenticated)
if params[:rejected]
if @registrant_verification.domain_registrant_delete_reject!("email link #{initiator}")
flash[:notice] = t(:registrant_domain_verification_rejected)
redirect_to registrant_domain_delete_confirm_path(@domain.id, rejected: true)
else
flash[:alert] = t(:registrant_domain_delete_rejected_failed)
return render 'show'
end
elsif params[:confirmed]
if @registrant_verification.domain_registrant_delete_confirm!("email link #{initiator}")
flash[:notice] = t(:registrant_domain_verification_confirmed)
redirect_to registrant_domain_delete_confirm_path(@domain.id, confirmed: true)
else
flash[:alert] = t(:registrant_domain_delete_confirmed_failed)
return render 'show'
end
confirmed = params[:confirmed] ? true : false
action = if confirmed
@registrant_verification.domain_registrant_delete_reject!("email link #{initiator}")
else
@registrant_verification.domain_registrant_delete_confirm!("email link #{initiator}")
end
fail_msg = t("registrant_domain_delete_#{confirmed ? 'confirmed' : 'rejected'}_failed".to_sym)
success_msg = t("registrant_domain_verification_#{confirmed ? 'confirmed' : 'rejected'}".to_sym)
flash[:alert] = action ? success_msg : fail_msg
(render 'show' && return) unless action
if confirmed
redirect_to registrant_domain_delete_confirm_path(@domain.id, confirmed: true) && return
else
redirect_to registrant_domain_delete_confirm_path(@domain.id, rejected: true) unless confirmed
end
end
end

View file

@ -31,6 +31,8 @@ class Registrant::DomainUpdateConfirmsController < RegistrantController
end
elsif params[:confirmed]
if @registrant_verification.domain_registrant_change_confirm!("email link, #{initiator}")
Dispute.close_by_domain(@domain.name) if @domain.disputed?
flash[:notice] = t(:registrant_domain_verification_confirmed)
redirect_to registrant_domain_update_confirm_path(@domain.id, confirmed: true)
else

View file

@ -100,12 +100,14 @@ class Registrar
authorize! :update, Depp::Domain
@data = @domain.info(params[:domain_name])
@domain_params = Depp::Domain.construct_params_from_server_data(@data)
@dispute = Dispute.active.find_by(domain_name: params[:domain_name])
end
def update
authorize! :update, Depp::Domain
@domain_params = params[:domain]
@data = @domain.update(@domain_params)
@dispute = Dispute.active.find_by(domain_name: @domain_params[:name])
if response_ok?
redirect_to info_registrar_domains_url(domain_name: @domain_params[:name])

View file

@ -0,0 +1,63 @@
class DisputeStatusUpdateJob < Que::Job
def run(logger: Logger.new(STDOUT))
@logger = logger
@backlog = { 'activated': 0, 'closed': 0, 'activate_fail': [], 'close_fail': [] }
.with_indifferent_access
close_disputes
activate_disputes
@logger.info "DisputeStatusUpdateJob - All done. Closed #{@backlog['closed']} and " \
"activated #{@backlog['activated']} disputes."
show_failed_disputes unless @backlog['activate_fail'].empty? && @backlog['close_fail'].empty?
end
def close_disputes
disputes = Dispute.where(closed: nil).where('expires_at < ?', Time.zone.today).all
@logger.info "DisputeStatusUpdateJob - Found #{disputes.count} closable disputes"
disputes.each do |dispute|
process_dispute(dispute, closing: true)
end
end
def activate_disputes
disputes = Dispute.where(closed: nil, starts_at: Time.zone.today).all
@logger.info "DisputeStatusUpdateJob - Found #{disputes.count} activatable disputes"
disputes.each do |dispute|
process_dispute(dispute, closing: false)
end
end
def process_dispute(dispute, closing: false)
intent = closing ? 'close' : 'activate'
success = closing ? dispute.close(initiator: 'Job') : dispute.generate_data
create_backlog_entry(dispute: dispute, intent: intent, successful: success)
end
def create_backlog_entry(dispute:, intent:, successful:)
if successful
@backlog["#{intent}d"] += 1
@logger.info "DisputeStatusUpdateJob - #{intent}d dispute " \
" for '#{dispute.domain_name}'"
else
@backlog["#{intent}_fail"] << dispute.id
@logger.info 'DisputeStatusUpdateJob - Failed to' \
"#{intent} dispute for '#{dispute.domain_name}'"
end
end
def show_failed_disputes
if @backlog['close_fail'].any?
@logger.info('DisputeStatusUpdateJob - Failed to close disputes with Ids:' \
"#{@backlog['close_fail']}")
end
return unless @backlog['activate_fail'].any?
@logger.info('DisputeStatusUpdateJob - Failed to activate disputes with Ids:' \
"#{@backlog['activate_fail']}")
end
end

View file

@ -1,13 +1,14 @@
class UpdateWhoisRecordJob < Que::Job
def run(names, type)
::PaperTrail.whodunnit = "job - #{self.class.name} - #{type}"
::PaperTrail.request.whodunnit = "job - #{self.class.name} - #{type}"
klass = case type
when 'reserved'then ReservedDomain
when 'blocked' then BlockedDomain
when 'domain' then Domain
end
when 'reserved' then ReservedDomain
when 'blocked' then BlockedDomain
when 'domain' then Domain
when 'disputed' then Dispute.active
end
Array(names).each do |name|
record = klass.find_by(name: name)
@ -19,8 +20,6 @@ class UpdateWhoisRecordJob < Que::Job
end
end
def update_domain(domain)
domain.whois_record ? domain.whois_record.save : domain.create_whois_record
end
@ -33,6 +32,9 @@ class UpdateWhoisRecordJob < Que::Job
update_reserved(record)
end
def update_disputed(record)
update_reserved(record)
end
# 1. deleting own
# 2. trying to regenerate reserved in order domain is still in the list
@ -41,14 +43,27 @@ class UpdateWhoisRecordJob < Que::Job
BlockedDomain.find_by(name: name).try(:generate_data)
ReservedDomain.find_by(name: name).try(:generate_data)
Dispute.active.find_by(domain_name: name).try(:generate_data)
end
def delete_reserved(name)
Domain.where(name: name).any?
Whois::Record.where(name: name).delete_all
remove_status_from_whois(domain_name: name, domain_status: 'Reserved')
end
def delete_blocked(name)
delete_reserved(name)
end
def delete_disputed(name)
return if Dispute.active.find_by(domain_name: name).present?
remove_status_from_whois(domain_name: name, domain_status: 'disputed')
end
def remove_status_from_whois(domain_name:, domain_status:)
Whois::Record.where(name: domain_name).each do |r|
r.json['status'] = r.json['status'].delete_if { |status| status == domain_status }
r.json['status'].blank? ? r.destroy : r.save
end
end
end

View file

@ -100,6 +100,7 @@ class Ability
can :manage, Invoice
can :manage, WhiteIp
can :manage, AccountActivity
can :manage, Dispute
can :read, ApiLog::EppLog
can :read, ApiLog::ReppLog
can :update, :pending

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Concerns
module Domain
module Disputable
extend ActiveSupport::Concern
included do
validate :validate_disputed
end
def mark_as_disputed
statuses.push(DomainStatus::DISPUTED) unless statuses.include?(DomainStatus::DISPUTED)
save
end
def unmark_as_disputed
statuses.delete_if { |status| status == DomainStatus::DISPUTED }
save
end
def in_disputed_list?
@in_disputed_list ||= Dispute.active.find_by(domain_name: name).present?
end
def disputed?
Dispute.active.where(domain_name: name).any?
end
def validate_disputed
return if persisted? || !in_disputed_list?
if reserved_pw.blank?
errors.add(:base, :required_parameter_missing_disputed)
return false
end
return if Dispute.valid_auth?(name, reserved_pw)
errors.add(:base, :invalid_auth_information_reserved)
end
end
end
end

View file

@ -0,0 +1,15 @@
module WhoisStatusPopulate
extend ActiveSupport::Concern
def generate_json(record, domain_status:)
h = HashWithIndifferentAccess.new(name: record.name, status: [domain_status])
return h if record.json.blank?
status_arr = (record.json['status'] ||= [])
return record.json if status_arr.include? domain_status
status_arr.push(domain_status)
record.json['status'] = status_arr
record.json
end
end

133
app/models/dispute.rb Normal file
View file

@ -0,0 +1,133 @@
class Dispute < ApplicationRecord
include WhoisStatusPopulate
validates :domain_name, :password, :starts_at, :expires_at, presence: true
before_validation :fill_empty_passwords, :set_expiry_date
validate :validate_domain_name_format
validate :validate_domain_name_period_uniqueness
validate :validate_start_date
before_save :set_expiry_date, :sync_reserved_password, :generate_data
after_destroy :remove_data
scope :expired, -> { where('expires_at < ?', Time.zone.today) }
scope :active, lambda {
where('starts_at <= ? AND expires_at >= ? AND closed IS NULL', Time.zone.today, Time.zone.today)
}
scope :closed, -> { where.not(closed: nil) }
attr_readonly :domain_name
def domain
Domain.find_by(name: domain_name)
end
def self.close_by_domain(domain_name)
dispute = Dispute.active.find_by(domain_name: domain_name)
return false unless dispute
dispute.close(initiator: 'Registrant')
end
def self.valid_auth?(domain_name, password)
Dispute.active.find_by(domain_name: domain_name, password: password).present?
end
def set_expiry_date
return if starts_at.blank?
self.expires_at = starts_at + Setting.dispute_period_in_months.months
end
def generate_password
self.password = SecureRandom.hex
end
def generate_data
return if starts_at > Time.zone.today || expires_at < Time.zone.today
domain&.mark_as_disputed
return if domain
wr = Whois::Record.find_or_initialize_by(name: domain_name)
wr.json = @json = generate_json(wr, domain_status: 'disputed')
wr.save
end
def close(initiator: 'Unknown')
return false unless update(closed: Time.zone.now, initiator: initiator)
return if Dispute.active.where(domain_name: domain_name).any?
domain&.unmark_as_disputed
return true if domain
forward_to_auction_if_possible
end
def forward_to_auction_if_possible
domain = DNS::DomainName.new(domain_name)
if domain.available? && domain.auctionable?
domain.sell_at_auction
return true
end
whois_record = Whois::Record.find_by(name: domain_name)
remove_whois_data(whois_record)
end
def remove_whois_data(record)
return true unless record
record.json['status'] = record.json['status'].delete_if { |status| status == 'disputed' }
record.destroy && return if record.json['status'].blank?
record.save
end
def remove_data
UpdateWhoisRecordJob.enqueue domain_name, 'disputed'
end
def fill_empty_passwords
generate_password if password.blank?
end
def sync_reserved_password
reserved_domain = ReservedDomain.find_by(name: domain_name)
generate_password if password.blank?
unless reserved_domain.nil?
reserved_domain.password = password
reserved_domain.save!
end
generate_data
end
private
def validate_start_date
return if starts_at.nil?
errors.add(:starts_at, :future) if starts_at.future?
end
def validate_domain_name_format
return unless domain_name
zone = domain_name.reverse.rpartition('.').map(&:reverse).reverse.last
supported_zone = DNS::Zone.origins.include?(zone)
errors.add(:domain_name, :unsupported_zone) unless supported_zone
end
def validate_domain_name_period_uniqueness
existing_dispute = Dispute.unscoped.where(domain_name: domain_name, closed: nil)
.where('expires_at >= ?', starts_at)
existing_dispute = existing_dispute.where.not(id: id) unless new_record?
return unless existing_dispute.any?
errors.add(:starts_at, 'Dispute already exists for this domain at given timeframe')
end
end

View file

@ -68,6 +68,10 @@ module DNS
ReservedDomain.where(name: name).any?
end
def disputed?
Dispute.active.where(domain_name: name).any?
end
def auctionable?
!not_auctionable?
end
@ -81,7 +85,7 @@ module DNS
attr_reader :name
def not_auctionable?
blocked? || reserved?
blocked? || reserved? || disputed?
end
def zone_with_same_origin?

View file

@ -9,6 +9,7 @@ class Domain < ApplicationRecord
include Concerns::Domain::Transferable
include Concerns::Domain::RegistryLockable
include Concerns::Domain::Releasable
include Concerns::Domain::Disputable
attr_accessor :roles
@ -88,8 +89,8 @@ class Domain < ApplicationRecord
validates :puny_label, length: { maximum: 63 }
validates :period, presence: true, numericality: { only_integer: true }
validates :transfer_code, presence: true
validate :validate_reservation
def validate_reservation
return if persisted? || !in_reserved_list?
@ -99,6 +100,7 @@ class Domain < ApplicationRecord
end
return if ReservedDomain.pw_for(name) == reserved_pw
errors.add(:base, :invalid_auth_information_reserved)
end
@ -282,20 +284,23 @@ class Domain < ApplicationRecord
def server_holdable?
return false if statuses.include?(DomainStatus::SERVER_HOLD)
return false if statuses.include?(DomainStatus::SERVER_MANUAL_INZONE)
true
end
def renewable?
if Setting.days_to_renew_domain_before_expire != 0
# if you can renew domain at days_to_renew before domain expiration
if (expire_time.to_date - Date.today) + 1 > Setting.days_to_renew_domain_before_expire
return false
end
blocking_statuses = [DomainStatus::DELETE_CANDIDATE, DomainStatus::PENDING_RENEW,
DomainStatus::PENDING_TRANSFER, DomainStatus::DISPUTED,
DomainStatus::PENDING_UPDATE, DomainStatus::PENDING_DELETE,
DomainStatus::PENDING_DELETE_CONFIRMATION]
return false if statuses.include_any? blocking_statuses
return true unless Setting.days_to_renew_domain_before_expire != 0
# if you can renew domain at days_to_renew before domain expiration
if (expire_time.to_date - Time.zone.today) + 1 > Setting.days_to_renew_domain_before_expire
return false
end
return false if statuses.include_any?(DomainStatus::DELETE_CANDIDATE, DomainStatus::PENDING_RENEW,
DomainStatus::PENDING_TRANSFER, DomainStatus::PENDING_DELETE,
DomainStatus::PENDING_UPDATE, DomainStatus::PENDING_DELETE_CONFIRMATION)
true
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
class DomainStatus < ApplicationRecord
include EppErrors
belongs_to :domain
@ -70,6 +72,7 @@ class DomainStatus < ApplicationRecord
FORCE_DELETE = 'serverForceDelete'
DELETE_CANDIDATE = 'deleteCandidate'
EXPIRED = 'expired'
DISPUTED = 'disputed'
STATUSES = [
CLIENT_DELETE_PROHIBITED, SERVER_DELETE_PROHIBITED, CLIENT_HOLD, SERVER_HOLD,
@ -78,19 +81,19 @@ class DomainStatus < ApplicationRecord
INACTIVE, OK, PENDING_CREATE, PENDING_DELETE, PENDING_DELETE_CONFIRMATION, PENDING_RENEW, PENDING_TRANSFER,
PENDING_UPDATE, SERVER_MANUAL_INZONE, SERVER_REGISTRANT_CHANGE_PROHIBITED,
SERVER_ADMIN_CHANGE_PROHIBITED, SERVER_TECH_CHANGE_PROHIBITED, FORCE_DELETE,
DELETE_CANDIDATE, EXPIRED
]
DELETE_CANDIDATE, EXPIRED, DISPUTED
].freeze
CLIENT_STATUSES = [
CLIENT_DELETE_PROHIBITED, CLIENT_HOLD, CLIENT_RENEW_PROHIBITED, CLIENT_TRANSFER_PROHIBITED,
CLIENT_UPDATE_PROHIBITED
]
].freeze
SERVER_STATUSES = [
SERVER_DELETE_PROHIBITED, SERVER_HOLD, SERVER_RENEW_PROHIBITED, SERVER_TRANSFER_PROHIBITED,
SERVER_UPDATE_PROHIBITED, SERVER_MANUAL_INZONE, SERVER_REGISTRANT_CHANGE_PROHIBITED,
SERVER_ADMIN_CHANGE_PROHIBITED, SERVER_TECH_CHANGE_PROHIBITED
]
].freeze
UPDATE_PROHIBIT_STATES = [
DomainStatus::PENDING_DELETE_CONFIRMATION,

View file

@ -53,12 +53,13 @@ class Epp::Domain < Domain
def epp_code_map
{
'2002' => [ # Command use error
[:base, :domain_already_belongs_to_the_querying_registrar]
%i[base domain_already_belongs_to_the_querying_registrar],
],
'2003' => [ # Required parameter missing
[:registrant, :blank],
[:registrar, :blank],
[:base, :required_parameter_missing_reserved]
%i[registrant blank],
%i[registrar blank],
%i[base required_parameter_missing_reserved],
%i[base required_parameter_missing_disputed],
],
'2004' => [ # Parameter value range error
[:dnskeys, :out_of_range,
@ -85,10 +86,11 @@ class Epp::Domain < Domain
[:puny_label, :too_long, { obj: 'name', val: name_puny }]
],
'2201' => [ # Authorisation error
[:transfer_code, :wrong_pw]
%i[transfer_code wrong_pw],
],
'2202' => [
[:base, :invalid_auth_information_reserved]
%i[base invalid_auth_information_reserved],
%i[base invalid_auth_information_disputed],
],
'2302' => [ # Object exists
[:name_dirty, :taken, { value: { obj: 'name', val: name_dirty } }],
@ -473,13 +475,35 @@ class Epp::Domain < Domain
self.up_date = Time.zone.now
end
same_registrant_as_current = (registrant.code == frame.css('registrant').text)
same_registrant_as_current = true
# registrant block may not be present, so we need this to rule out false positives
if frame.css('registrant').text.present?
same_registrant_as_current = (registrant.code == frame.css('registrant').text)
end
if !same_registrant_as_current && disputed?
disputed_pw = frame.css('reserved > pw').text
if disputed_pw.blank?
add_epp_error('2304', nil, nil, 'Required parameter missing; reserved' \
'pw element required for dispute domains')
else
dispute = Dispute.active.find_by(domain_name: name, password: disputed_pw)
if dispute
Dispute.close_by_domain(name)
else
add_epp_error('2202', nil, nil, 'Invalid authorization information; '\
'invalid reserved>pw value')
end
end
end
unverified_registrant_params = frame.css('registrant').present? &&
frame.css('registrant').attr('verified').to_s.downcase != 'yes'
if !same_registrant_as_current && errors.empty? && verify &&
Setting.request_confrimation_on_registrant_change_enabled &&
frame.css('registrant').present? &&
frame.css('registrant').attr('verified').to_s.downcase != 'yes'
registrant_verification_asked!(frame.to_s, current_user.id)
unverified_registrant_params
registrant_verification_asked!(frame.to_s, current_user.id) unless disputed?
end
errors.empty? && super(at)
@ -706,6 +730,11 @@ class Epp::Domain < Domain
def can_be_deleted?
if disputed?
errors.add(:base, :domain_status_prohibits_operation)
return false
end
begin
errors.add(:base, :domain_status_prohibits_operation)
return false

View file

@ -1,7 +1,9 @@
class ReservedDomain < ApplicationRecord
include Versions # version/reserved_domain_version.rb
include WhoisStatusPopulate
before_save :fill_empty_passwords
before_save :generate_data
before_save :sync_dispute_password
after_destroy :remove_data
validates :name, domain_name: true, uniqueness: true
@ -41,23 +43,21 @@ class ReservedDomain < ApplicationRecord
self.password = SecureRandom.hex
end
def sync_dispute_password
dispute = Dispute.active.find_by(domain_name: name)
self.password = dispute.password if dispute.present?
end
def generate_data
return if Domain.where(name: name).any?
wr = Whois::Record.find_or_initialize_by(name: name)
wr.json = @json = generate_json # we need @json to bind to class
wr.json = @json = generate_json(wr, domain_status: 'Reserved') # we need @json to bind to class
wr.save
end
alias_method :update_whois_record, :generate_data
def generate_json
h = HashWithIndifferentAccess.new
h[:name] = self.name
h[:status] = ['Reserved']
h
end
def remove_data
UpdateWhoisRecordJob.enqueue name, 'reserved'
end

View file

@ -46,6 +46,7 @@ class Setting < RailsSettings::Base
expire_warning_period
redemption_grace_period
expire_pending_confirmation
dispute_period_in_months
]
end

View file

@ -84,6 +84,7 @@ class WhoisRecord < ApplicationRecord
def populate
return if domain_id.blank?
self.json = generated_json
self.name = json['name']
self.registrar_id = domain.registrar_id if domain # for faster registrar updates

View file

@ -32,10 +32,11 @@
%li= link_to t('.zones'), admin_zones_path
%li= link_to t('.blocked_domains'), admin_blocked_domains_path
%li= link_to t('.reserved_domains'), admin_reserved_domains_path
%li= link_to t('.disputed_domains'), admin_disputes_path
%li= link_to t('.epp_log'), admin_epp_logs_path(created_after: 'today')
%li= link_to t('.repp_log'), admin_repp_logs_path(created_after: 'today')
%li= link_to t('.que'), '/admin/que'
%ul.nav.navbar-nav.navbar-right
%li= link_to t('.sign_out'), destroy_admin_user_session_path, method: :delete,
class: 'navbar-link'
class: 'navbar-link'

View file

@ -0,0 +1,60 @@
<%= form_for([:admin, @dispute], html: { class: 'form-horizontal' }) do |f| %>
<%= render 'shared/full_errors', object: @dispute %>
<div class="row">
<div class="col-md-8">
<div class="panel panel-default">
<div class="panel-heading clearfix">
<div class="pull-left">
<%= t(:general) %>
</div>
</div>
<div class="panel-body">
<div>
<p>As per domain law, expiry time is <%= Setting.dispute_period_in_months / 12 %> years ahead from start date.</p>
</div>
<div class="form-group">
<div class="col-md-4 control-label">
<%= f.label :domain_name %>
</div>
<div class="col-md-7">
<%= f.text_field(:domain_name, class: 'form-control', disabled: !f.object.new_record?) %>
</div>
</div>
<div class="form-group">
<div class="col-md-4 control-label">
<%= f.label :password %>
</div>
<div class="col-md-7">
<%= f.text_field(:password, placeholder: t(:optional), class: 'form-control') %>
<span class="help-block"><%= t '.password_hint' %></span>
</div>
</div>
<div class="form-group">
<div class="col-md-4 control-label">
<%= f.label :starts_at %>
</div>
<div class="col-md-7">
<%= f.text_field(:starts_at, class: 'form-control js-datepicker') %>
<span class="help-block"><%= t '.past_or_today' %></span>
</div>
</div>
<div class="form-group">
<div class="col-md-4 control-label">
<%= f.label :comment %>
</div>
<div class="col-md-7">
<%= f.text_field(:comment, placeholder: t(:optional), class: 'form-control') %>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 text-right">
<%= button_tag(t(:save), class: 'btn btn-primary') %>
</div>
</div>
<% end %>

View file

@ -0,0 +1,3 @@
= render 'shared/title', name: t(:edit_dispute)
= render 'form'

View file

@ -0,0 +1,175 @@
<% content_for :actions do %>
<%= link_to(t('.new_btn'), new_admin_dispute_path, class: 'btn btn-primary') %>
<% end %>
<%= render 'shared/title', name: t('.title') %>
<div class="row">
<div class="col-md-12">
<%= search_form_for [:admin, @q], html: { style: 'margin-bottom: 0;', class: 'js-form', autocomplete: 'off' } do |f| %>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<%= f.label :domain_name %>
<%= f.search_field :domain_name_matches, value: params[:q][:domain_name_matches], class: 'form-control', placeholder: t(:name) %>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<%= f.label t(:created_at_from) %>
<%= f.search_field :created_at_gteq, value: params[:q][:created_at_gteq], class: 'form-control js-datepicker', placeholder: t(:created_at_from) %>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<%= f.label t(:created_at_until) %>
<%= f.search_field :created_at_lteq, value: params[:q][:created_at_lteq], class: 'form-control js-datepicker', placeholder: t(:created_at_until) %>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<%= label_tag t(:results_per_page) %>
<%= text_field_tag :results_per_page, params[:results_per_page], class: 'form-control', placeholder: t(:results_per_page) %>
</div>
</div>
<div class="col-md-3" style="padding-top: 25px;">
<button class="btn btn-primary">
&nbsp;
<span class="glyphicon glyphicon-search"></span>
&nbsp;
</button>
<%= link_to(t('.reset_btn'), admin_disputes_path, class: 'btn btn-default') %>
</div>
</div>
<% end %>
</div>
</div>
<hr />
<p>Active disputes</p>
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-hover table-bordered table-condensed">
<thead>
<tr>
<th class="col-xs-2">
<%= sort_link(@q, 'domain_name') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'password') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'starts_at') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'expires_at') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'comment') %>
</th>
<th class="col-xs-2">
<%= t(:actions) %>
</th>
</tr>
</thead>
<tbody>
<% @disputes.each do |x| %>
<tr>
<td>
<%= x.domain_name %>
</td>
<td>
<%= x.password %>
</td>
<td>
<%= x.starts_at %>
</td>
<td>
<%= x.expires_at %>
</td>
<td>
<%= x.comment %>
</td>
<td>
<%= link_to t(:edit), edit_admin_dispute_path(id: x.id),
class: 'btn btn-primary btn-xs' %>
<%= link_to t(:delete), delete_admin_dispute_path(id: x.id),
data: { confirm: t(:are_you_sure) }, class: 'btn btn-danger btn-xs' %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<%= paginate @disputes %>
</div>
<div class="col-md-6 text-right">
<div class="pagination">
<%= t(:result_count, count: @disputes.total_count) %>
</div>
</div>
</div>
<hr />
<p>Expired / Closed disputes</p>
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-hover table-bordered table-condensed">
<thead>
<tr>
<th class="col-xs-2">
<%= sort_link(@q, 'domain_name') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'initiator') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'starts_at') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'closed') %>
</th>
<th class="col-xs-2">
<%= sort_link(@q, 'comment') %>
</th>
</tr>
</thead>
<tbody>
<% @closed_disputes.each do |x| %>
<tr>
<td>
<%= x.domain_name %>
</td>
<td>
<%= x.initiator %>
</td>
<td>
<%= x.starts_at %>
</td>
<td>
<%= x.closed %>
</td>
<td>
<%= x.comment %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<%= paginate @closed_disputes, param_name: :closed_page %>
</div>
<div class="col-md-6 text-right">
<div class="pagination">
<%= t(:result_count, count: @closed_disputes.total_count) %>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
= render 'shared/title', name: t(:add_disputed_domain)
= render 'form'

View file

@ -48,7 +48,7 @@
= render 'setting_row', var: :request_confrimation_on_registrant_change_enabled
= render 'setting_row', var: :request_confirmation_on_domain_deletion_enabled
= render 'setting_row', var: :address_processing
= render 'setting_row', var: :dispute_period_in_months
%tr
%td.col-md-6= label_tag :default_language
%td.col-md-6

View file

@ -31,7 +31,7 @@
.col-md-7
= check_box_tag 'domain[verified]', '1', @domain_params[:verified].eql?('1'), onclick: "return (confirm('#{t(:verified_confirm)}') ? true : false);"
- unless params[:domain_name]
- if !params[:domain_name] || @dispute.present?
.form-group
.col-md-3.control-label
= label_tag :domain_reserved_pw, t(:reserved_pw)

View file

@ -44,6 +44,8 @@ defaults: &defaults
registrar_ip_whitelist_enabled: false
api_ip_whitelist_enabled: false
dispute_period_in_months: 36
registry_juridical_name: "Eesti Interneti SA"
registry_reg_no: "90010019"
registry_email: "info@internet.ee"

View file

@ -0,0 +1,19 @@
en:
activerecord:
errors:
models:
dispute:
attributes:
starts_at:
future: 'can not be greater than today'
admin:
disputes:
index:
title: Disputed domains
new_btn: New disputed domain
reset_btn: Reset
form:
password_hint: Generated automatically if left blank
optional: Not required by default
past_or_today: Can not be greater than today's date

View file

@ -13,6 +13,7 @@ en:
zones: Zones
blocked_domains: Blocked domains
reserved_domains: Reserved domains
disputed_domains: Disputed domains
epp_log: EPP log
repp_log: REPP log
que: Que

View file

@ -24,6 +24,8 @@ en:
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'
domain_name_blocked: 'Data management policy violation: Domain name is blocked [name]'
name_dirty:
invalid: 'Domain name is invalid'
@ -629,7 +631,9 @@ en:
available_verification_url_not_found: 'Available verification url not found, for domain.'
add_reserved_domain: 'Add domain to reserved list'
add_blocked_domain: 'Add domain to blocked list'
add_disputed_domain: 'Add domain to disputed list'
edit_pw: 'Edit Pw'
edit_dispute: 'Edit dispute'
optional: 'Optional'
test_registrar: "Test registrar"
verified_confirm: 'Verified status is for cases when current registrant is the one applying for the update. Legal document signed by the registrant is required. Are you sure this update is properly verified with the registrant?'

View file

@ -271,6 +271,11 @@ Rails.application.routes.draw do
get 'delete'
end
end
resources :disputes do
member do
get 'delete'
end
end
resources :registrars do
resources :api_users, except: %i[index]

View file

@ -0,0 +1,14 @@
class CreateDisputes < ActiveRecord::Migration[5.2]
def change
create_table :disputes do |t|
t.string :domain_name, null: false
t.string :password, null: false
t.date :expires_at, null: false
t.date :starts_at, null: false
t.text :comment
t.boolean :closed, null: false, default: false
t.timestamps
end
end
end

View file

@ -0,0 +1,5 @@
class AddDisputePeriodInMonthsToSetting < ActiveRecord::Migration[5.2]
def change
Setting.create(var: 'dispute_period_in_months', value: 36)
end
end

View file

@ -0,0 +1,19 @@
class AddClosedDateTimeAndUpdatorToDispute < ActiveRecord::Migration[5.2]
def up
rename_column :disputes, :closed, :closed_boolean
add_column :disputes, :closed, :datetime
execute 'UPDATE disputes SET closed = updated_at WHERE closed_boolean = true'
execute 'UPDATE disputes SET closed = NULL WHERE closed_boolean = false'
remove_column :disputes, :closed_boolean
add_column :disputes, :initiator, :string
end
def down
rename_column :disputes, :closed, :closed_datetime
add_column :disputes, :closed, :boolean, null: false, default: false
execute 'UPDATE disputes SET closed = true WHERE closed_datetime != NULL'
execute 'UPDATE disputes SET closed = false WHERE closed_datetime = NULL'
remove_column :disputes, :closed_datetime
remove_column :disputes, :initiator
end
end

View file

@ -1,6 +1,6 @@
--
-- PostgreSQL database dump
--
---
--- PostgreSQL database dump
---
SET statement_timeout = 0;
SET lock_timeout = 0;
@ -594,6 +594,43 @@ CREATE SEQUENCE public.directos_id_seq
ALTER SEQUENCE public.directos_id_seq OWNED BY public.directos.id;
--
-- Name: disputes; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
CREATE TABLE public.disputes (
id bigint NOT NULL,
domain_name character varying NOT NULL,
password character varying NOT NULL,
expires_at date NOT NULL,
starts_at date NOT NULL,
comment text,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
closed timestamp without time zone,
initiator character varying
);
--
-- Name: disputes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.disputes_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: disputes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.disputes_id_seq OWNED BY public.disputes.id;
--
-- Name: dnskeys; Type: TABLE; Schema: public; Owner: -; Tablespace:
--
@ -2415,6 +2452,13 @@ ALTER TABLE ONLY public.contacts ALTER COLUMN id SET DEFAULT nextval('public.con
ALTER TABLE ONLY public.directos ALTER COLUMN id SET DEFAULT nextval('public.directos_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.disputes ALTER COLUMN id SET DEFAULT nextval('public.disputes_id_seq'::regclass);
--
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
--
@ -2811,6 +2855,14 @@ ALTER TABLE ONLY public.directos
ADD CONSTRAINT directos_pkey PRIMARY KEY (id);
--
-- Name: disputes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
ALTER TABLE ONLY public.disputes
ADD CONSTRAINT disputes_pkey PRIMARY KEY (id);
--
-- Name: dnskeys_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
--
@ -4467,5 +4519,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20200204103125'),
('20200311114649'),
('20200417075720'),
('20200505103316');
('20200421093637'),
('20200505103316'),
('20200505150413'),
('20200518104105');

View file

@ -35,6 +35,11 @@ namespace :whois do
ReservedDomain.find_in_batches.each do |group|
UpdateWhoisRecordJob.enqueue group.map(&:name), 'reserved'
end
print "\n-----> Update disputed domains whois_records"
Dispute.active.find_in_batches.each do |group|
UpdateWhoisRecordJob.enqueue group.map(&:domain_name), 'disputed'
end
end
puts "\n-----> all done in #{(Time.zone.now.to_f - start).round(2)} seconds"
end

22
test/fixtures/disputes.yml vendored Normal file
View file

@ -0,0 +1,22 @@
active:
domain_name: active-dispute.test
password: active-001
starts_at: <%= Date.parse '2010-07-05' %>
expires_at: <%= Date.parse '2013-07-05' %>
future:
domain_name: future-dispute.test
password: active-001
starts_at: <%= Date.parse '2010-10-05' %>
expires_at: <%= Date.parse '2013-10-05' %>
expired:
domain_name: expired-dispute.test
password: active-001
starts_at: <%= Date.parse '2010-07-05' %>
expires_at: <%= Date.parse '2013-07-05' %>
closed: <%= Date.parse '2013-07-05' %>
closed:
domain_name: closed_dispute.test
password: active-001
starts_at: <%= Date.parse '2010-07-05' %>
expires_at: <%= Date.parse '2013-07-05' %>
closed: <%= Date.parse '2013-07-05' %>

View file

@ -0,0 +1,91 @@
require 'application_system_test_case'
require 'test_helper'
class AdminDisputesSystemTest < ApplicationSystemTestCase
include ActionView::Helpers::NumberHelper
setup do
@dispute = disputes(:active)
@original_default_language = Setting.default_language
sign_in users(:admin)
end
teardown do
Setting.default_language = @original_default_language
end
def test_creates_new_dispute
assert_nil Dispute.active.find_by(domain_name: 'hospital.test')
visit admin_disputes_path
click_on 'New disputed domain'
fill_in 'Domain name', with: 'hospital.test'
fill_in 'Password', with: '1234'
fill_in 'Starts at', with: (Time.zone.today - 2.years).to_s
fill_in 'Comment', with: 'Sample comment'
click_on 'Save'
assert_text 'Dispute was successfully created.'
assert_text 'hospital.test'
end
def test_creates_new_dispute_for_unregistered_domain
assert_nil Dispute.active.find_by(domain_name: 'nonexistant.test')
visit admin_disputes_path
click_on 'New disputed domain'
fill_in 'Domain name', with: 'nonexistant.test'
fill_in 'Password', with: '1234'
fill_in 'Starts at', with: Time.zone.today.to_s
fill_in 'Comment', with: 'Sample comment'
click_on 'Save'
assert_text 'Dispute was successfully created for domain that is not registered.'
assert_text 'nonexistant.test'
end
def test_throws_error_if_starts_at_is_in_future
assert_nil Dispute.active.find_by(domain_name: 'disputed.test')
visit admin_disputes_path
click_on 'New disputed domain'
fill_in 'Domain name', with: 'disputed.test'
fill_in 'Password', with: '1234'
fill_in 'Starts at', with: (Time.zone.today + 2.day).to_s
fill_in 'Comment', with: 'Sample comment'
click_on 'Save'
assert_text "Can not be greater than today's date"
end
def test_updates_dispute
assert_not_equal Time.zone.today, @dispute.starts_at
visit edit_admin_dispute_path(@dispute)
fill_in 'Starts at', with: Time.zone.today.to_s
click_link_or_button 'Save'
assert_text 'Dispute was successfully updated'
assert_text Time.zone.today
end
def test_deletes_dispute
visit delete_admin_dispute_path(@dispute)
assert_text 'Dispute was successfully closed.'
end
def test_can_not_create_overlapping_dispute
visit admin_disputes_path
click_on 'New disputed domain'
fill_in 'Domain name', with: 'active-dispute.test'
fill_in 'Starts at', with: @dispute.starts_at + 1.day
click_on 'Save'
assert_text 'Dispute already exists for this domain at given timeframe'
end
end

View file

@ -0,0 +1,70 @@
require "test_helper"
class DisputeStatusUpdateJobTest < ActiveSupport::TestCase
setup do
travel_to Time.zone.parse('2010-10-05')
@logger = Rails.logger
end
def test_nothing_is_raised
assert_nothing_raised do
DisputeStatusUpdateJob.run(logger: @logger)
end
end
def test_whois_data_added_when_dispute_activated
dispute = disputes(:future)
DisputeStatusUpdateJob.run(logger: @logger)
whois_record = Whois::Record.find_by(name: dispute.domain_name)
assert whois_record.present?
assert_includes whois_record.json['status'], 'disputed'
end
def test_on_expiry_unregistered_domain_is_sent_to_auction
dispute = disputes(:active)
dispute.update!(starts_at: Time.zone.today - 3.years - 1.day)
DisputeStatusUpdateJob.run(logger: @logger)
dispute.reload
assert dispute.closed
whois_record = Whois::Record.find_by(name: dispute.domain_name)
assert_equal ['AtAuction'], whois_record.json['status']
end
def test_registered_domain_whois_data_is_added
Dispute.create(domain_name: 'shop.test', starts_at: '2010-07-05')
travel_to Time.zone.parse('2010-07-05')
DisputeStatusUpdateJob.run(logger: @logger)
whois_record = Whois::Record.find_by(name: 'shop.test')
assert_includes whois_record.json['status'], 'disputed'
end
def test_registered_domain_whois_data_is_removed
travel_to Time.zone.parse('2010-07-05')
domain = domains(:shop)
domain.update(valid_to: Time.zone.parse('2015-07-05').to_s(:db),
outzone_at: Time.zone.parse('2015-07-06').to_s(:db),
delete_date: nil,
force_delete_date: nil)
# Dispute status is added automatically if starts_at is not in future
Dispute.create(domain_name: 'shop.test', starts_at: Time.zone.parse('2010-07-05'))
domain.reload
whois_record = Whois::Record.find_by(name: 'shop.test')
assert_includes whois_record.json['status'], 'disputed'
# Dispute status is removed night time day after it's ended
travel_to Time.zone.parse('2010-07-05') + 3.years + 1.day
DisputeStatusUpdateJob.run(logger: @logger)
whois_record.reload
assert_not whois_record.json['status'].include? 'disputed'
end
end

View file

@ -0,0 +1,52 @@
require 'test_helper'
class DisputedDomainTest < ActiveSupport::TestCase
setup do
@dispute = disputes(:active)
end
def test_fixture_is_valid
assert @dispute.valid?
end
def test_can_be_closed_by_domain_name
travel_to Time.zone.parse('2010-10-05')
Dispute.close_by_domain(@dispute.domain_name)
@dispute.reload
assert @dispute.closed
end
def test_syncs_password_to_reserved
dispute = Dispute.new(domain_name: 'reserved.test', starts_at: Time.zone.today, password: 'disputepw')
dispute.save
dispute.reload
assert_equal dispute.password, ReservedDomain.find_by(name: dispute.domain_name).password
end
def test_domain_name_zone_is_validated
dispute = Dispute.new(domain_name: 'correct.test', starts_at: Time.zone.today)
assert dispute.valid?
dispute.domain_name = 'zone.is.unrecognized.test'
assert_not dispute.valid?
end
def test_dispute_can_not_be_created_if_another_active_is_present
dispute = Dispute.new(domain_name: @dispute.domain_name,
starts_at: @dispute.starts_at + 1.day)
assert_not dispute.valid?
end
def test_expires_at_date_is_appended_automatically
dispute = Dispute.new(domain_name: 'random.test', starts_at: Time.zone.today)
assert dispute.valid?
assert_equal dispute.expires_at, dispute.starts_at + 3.years
end
def test_starts_at_must_be_present
dispute = Dispute.new(domain_name: 'random.test')
assert_not dispute.valid?
end
end

View file

@ -1,6 +1,6 @@
require 'application_system_test_case'
class BankStatementTest < ApplicationSystemTestCase
class AdminBankStatementsSystemTest < ApplicationSystemTestCase
setup do
sign_in users(:admin)
travel_to Time.zone.parse('2010-07-05 00:30:00')