diff --git a/app/assets/javascripts/admin/reports.js b/app/assets/javascripts/admin/reports.js index e96e26d717..0b462f6d7b 100644 --- a/app/assets/javascripts/admin/reports.js +++ b/app/assets/javascripts/admin/reports.js @@ -28,14 +28,21 @@ const groups = parameterContainer.querySelectorAll('.parameter-group'); const newIndex = groups.length; const template = groups[0].cloneNode(true); - + // Update all input names and values template.querySelectorAll('input').forEach(input => { const name = input.getAttribute('name'); input.setAttribute('name', name.replace(/\[\d+\]/, `[${newIndex}]`)); input.value = input.defaultValue; // Reset to default value }); - + + // Update all select names and reset to default values + template.querySelectorAll('select').forEach(select => { + const name = select.getAttribute('name'); + select.setAttribute('name', name.replace(/\[\d+\]/, `[${newIndex}]`)); + select.selectedIndex = 0; // Reset to first option (default) + }); + parameterContainer.appendChild(template); updateParameterSetTitles(); updateRemoveButtonVisibility(); diff --git a/app/jobs/expire_certificate_reminder_job.rb b/app/jobs/expire_certificate_reminder_job.rb new file mode 100644 index 0000000000..196c53d1a8 --- /dev/null +++ b/app/jobs/expire_certificate_reminder_job.rb @@ -0,0 +1,23 @@ +class ExpireCertificateReminderJob < ApplicationJob + queue_as :default + + DEADLINE = 1.month + + def perform + Certificate.where('expires_at < ?', DEADLINE.from_now).each do |certificate| + send_reminder(certificate) + end + end + + private + + def send_reminder(certificate) + registrar = certificate.api_user.registrar + + send_email(registrar, certificate) + end + + def send_email(registrar, certificate) + CertificateMailer.certificate_expiring(email: registrar.email, certificate: certificate).deliver_now + end +end diff --git a/app/mailers/certificate_mailer.rb b/app/mailers/certificate_mailer.rb index da340228e4..6a164547ee 100644 --- a/app/mailers/certificate_mailer.rb +++ b/app/mailers/certificate_mailer.rb @@ -12,4 +12,10 @@ def signed(email:, api_user:, crt:) subject = 'Certificate Signing Confirmation' mail(to: email, subject: subject) end + + def certificate_expiring(email:, certificate:) + @certificate = certificate + subject = 'Certificate Expiring' + mail(to: email, subject: subject) + end end diff --git a/app/services/report_runner.rb b/app/services/report_runner.rb index da5bb2ab1f..205354e928 100644 --- a/app/services/report_runner.rb +++ b/app/services/report_runner.rb @@ -38,7 +38,17 @@ def execute_report(report, params) if params.present? params.each_value do |parameter_set| - permitted_param_set = parameter_set.permit(report_parameters.map { |param| param["name"] }) + # Build permit list - for array parameters (type: 'registrars'), we need to specify them as { param_name: [] } + permit_list = report_parameters.map do |param| + # Check if parameter type indicates an array (registrars, or any type ending with 's' that suggests plural) + if param["type"] == "registrars" || (param["type"]&.end_with?("s") && param["type"] != "date") + { param["name"] => [] } + else + param["name"] + end + end + + permitted_param_set = parameter_set.permit(*permit_list) query = report.sql_query.dup handle_parameters(query, permitted_param_set) results << run_query(query) @@ -79,10 +89,10 @@ def monitor_thread(thread) def run_query(query) if Rails.env.test? - ActiveRecord::Base.connection.exec_query(sanitize_sql(query)) + ActiveRecord::Base.connection.exec_query(query) else ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do - ActiveRecord::Base.connection.exec_query(sanitize_sql(query)) + ActiveRecord::Base.connection.exec_query(query) end end rescue StandardError => e @@ -90,23 +100,35 @@ def run_query(query) raise e end - def sanitize_sql(query) - ActiveRecord::Base.sanitize_sql_array([query]) - end - def handle_parameters(query, param_set) parameter_values = [] param_set.each_key do |param| value = param_set[param] substitute_query_param(query, param, value) - parameter_values << "#{param.humanize}: #{value}" + parameter_values << "#{param.humanize}: #{value}" if value.present? end parameter_values end def substitute_query_param(query, param, value) - query.gsub!(":#{param}", ActiveRecord::Base.connection.quote(value)) + if value.blank? + # Replace :param with NULL for SQL compatibility + query.gsub!(":#{param}", "NULL") + elsif value.is_a?(Array) + # Handle array values (from multiselect) - convert to PostgreSQL array format + # For integer arrays (like registrar_ids), we need integers without quotes + if param.to_s.include?('id') && value.all? { |v| v.to_s.match?(/^\d+$/) } + array_values = value.map(&:to_i).join(',') + query.gsub!(":#{param}", "ARRAY[#{array_values}]") + else + # For string arrays, use quotes + array_values = value.map { |v| ActiveRecord::Base.connection.quote(v) }.join(',') + query.gsub!(":#{param}", "ARRAY[#{array_values}]") + end + else + query.gsub!(":#{param}", ActiveRecord::Base.connection.quote(value)) + end end def build_page_title(report, params) diff --git a/app/views/admin/reports/partials/_parameters_form.haml b/app/views/admin/reports/partials/_parameters_form.haml index 17057fb53f..e33549e875 100644 --- a/app/views/admin/reports/partials/_parameters_form.haml +++ b/app/views/admin/reports/partials/_parameters_form.haml @@ -22,6 +22,12 @@ = date_field_tag "report_parameters[#{set_index}][#{param["name"]}]", (set_values[param["name"]] || param["default"]), class: 'form-control' - elsif param["type"] == "string" = text_field_tag "report_parameters[#{set_index}][#{param["name"]}]", (set_values[param["name"]] || param["default"]), class: 'form-control' + - elsif param["type"] == "registrar" + = select_tag "report_parameters[#{set_index}][#{param["name"]}]", options_from_collection_for_select(Registrar.order(:name), :id, :name, (set_values[param["name"]] || param["default"])), { include_blank: 'All Registrars', class: 'form-control' } + - elsif param["type"] == "registrars" + - selected_ids = (set_values[param["name"]] || param["default"] || []) + - selected_ids = selected_ids.is_a?(String) ? selected_ids.split(',').map(&:strip) : selected_ids + = select_tag "report_parameters[#{set_index}][#{param["name"]}][]", options_from_collection_for_select(Registrar.order(:name), :id, :name, selected_ids), { multiple: true, class: 'form-control js-combobox', include_blank: 'Select registrars (Ctrl/Cmd+Click for multiple)' } .d-flex.flex-wrap.gap-2 %button.btn.btn-secondary#add-parameter-group{ type: 'button' }= t('.add_parameter_set') diff --git a/app/views/mailers/certificate_mailer/certificate_expiring.html.erb b/app/views/mailers/certificate_mailer/certificate_expiring.html.erb new file mode 100644 index 0000000000..d80996f8f8 --- /dev/null +++ b/app/views/mailers/certificate_mailer/certificate_expiring.html.erb @@ -0,0 +1,97 @@ + + +
+ + +Your certificate is approaching its expiration date and requires attention.
++ Important: If you don't renew your certificate before it expires, your services may become unavailable. +
+ + + + diff --git a/app/views/mailers/certificate_mailer/certificate_expiring.text.erb b/app/views/mailers/certificate_mailer/certificate_expiring.text.erb new file mode 100644 index 0000000000..efbf29b1c2 --- /dev/null +++ b/app/views/mailers/certificate_mailer/certificate_expiring.text.erb @@ -0,0 +1,33 @@ +CERTIFICATE EXPIRING SOON - ACTION REQUIRED +=========================================== + +⚠️ WARNING: Your certificate is approaching its expiration date and requires immediate attention. + +CERTIFICATE DETAILS: +-------------------- +Common Name: <%= @certificate.common_name %> +Serial Number: <%= @certificate.serial %> +Interface: <%= @certificate.interface.capitalize %> +Expires At: <%= @certificate.expires_at.strftime("%B %d, %Y at %H:%M UTC") %> +<% days_left = (@certificate.expires_at.to_date - Date.current).to_i -%> +Days Left: <%= days_left %> days <%= days_left <= 7 ? '(CRITICAL!)' : '(WARNING)' %> + +REQUIRED ACTIONS: +----------------- +1. Generate a new Certificate Signing Request (CSR) +2. Submit the CSR through your registrar interface +3. Install the new certificate before the current one expires + +IMPORTANT NOTICE: +----------------- +If you don't renew your certificate before it expires, your services may become +unavailable and cause disruption to your operations. + +Please take immediate action to avoid service interruption. + +--- +This is an automated notification from the Registry System. +If you have any questions, please contact your registry administrator. + +Certificate ID: <%= @certificate.id %> +Generated: <%= Time.current.strftime("%Y-%m-%d %H:%M UTC") %> diff --git a/test/jobs/expire_certificate_reminder_job_test.rb b/test/jobs/expire_certificate_reminder_job_test.rb new file mode 100644 index 0000000000..59df68afc0 --- /dev/null +++ b/test/jobs/expire_certificate_reminder_job_test.rb @@ -0,0 +1,50 @@ +require 'test_helper' + +class ExpireCertificateReminderJobTest < ActiveJob::TestCase + include ActionMailer::TestHelper + + setup do + ActionMailer::Base.deliveries.clear + @certificate = certificates(:api) + end + + def test_sends_reminder_for_expiring_certificate + # Устанавливаем дату истечения на 2 недели от текущего времени (меньше месяца) + @certificate.update(expires_at: 2.weeks.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 1 + + # Проверяем, что письмо отправлено правильному получателю + email = ActionMailer::Base.deliveries.last + assert_equal @certificate.api_user.registrar.email, email.to.first + assert_match 'Certificate Expiring', email.subject + end + + def test_does_not_send_reminder_for_certificate_expiring_later + # Устанавливаем дату истечения на 2 месяца от текущего времени (больше месяца) + @certificate.update(expires_at: 2.months.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 0 + end + + def test_sends_reminder_for_multiple_expiring_certificates + # Создаем второй сертификат, который тоже скоро истекает + second_certificate = certificates(:registrar) + @certificate.update(expires_at: 1.week.from_now) + second_certificate.update(expires_at: 3.weeks.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 2 + end +end