Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/assets/javascripts/admin/reports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
23 changes: 23 additions & 0 deletions app/jobs/expire_certificate_reminder_job.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions app/mailers/certificate_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 31 additions & 9 deletions app/services/report_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -79,34 +89,46 @@ 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
Rails.logger.error("Query Error: #{e.message}\nQuery: #{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)
Expand Down
6 changes: 6 additions & 0 deletions app/views/admin/reports/partials/_parameters_form.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
97 changes: 97 additions & 0 deletions app/views/mailers/certificate_mailer/certificate_expiring.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Certificate Expiring</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background-color: #f8f9fa;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.certificate-details {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
font-size: 14px;
color: #6c757d;
}
.btn {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 5px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="header">
<h1>Certificate Expiring Soon</h1>
<p>Your certificate is approaching its expiration date and requires attention.</p>
</div>

<div class="alert">
<strong>⚠️ Action Required:</strong> Your certificate will expire soon. Please renew it to avoid service interruption.
</div>

<div class="certificate-details">
<h3>Certificate Details:</h3>
<ul>
<li><strong>Common Name:</strong> <%= @certificate.common_name %></li>
<li><strong>Serial Number:</strong> <%= @certificate.serial %></li>
<li><strong>Interface:</strong> <%= @certificate.interface.capitalize %></li>
<li><strong>Expires At:</strong> <span style="color: #dc3545; font-weight: bold;"><%= @certificate.expires_at.strftime("%B %d, %Y at %H:%M UTC") %></span></li>
<li><strong>Days Until Expiration:</strong>
<% days_left = (@certificate.expires_at.to_date - Date.current).to_i %>
<span style="color: <%= days_left <= 7 ? '#dc3545' : '#ffc107' %>; font-weight: bold;">
<%= days_left %> days
</span>
</li>
</ul>
</div>

<h3>What you need to do:</h3>
<ol>
<li>Generate a new Certificate Signing Request (CSR)</li>
<li>Submit the CSR through your registrar interface</li>
<li>Install the new certificate before the current one expires</li>
</ol>

<p>
<strong>Important:</strong> If you don't renew your certificate before it expires, your services may become unavailable.
</p>

<div class="footer">
<p>This is an automated notification from the Registry System.</p>
<p>If you have any questions, please contact your registry administrator.</p>
<p><small>Certificate ID: <%= @certificate.id %> | Generated: <%= Time.current.strftime("%Y-%m-%d %H:%M UTC") %></small></p>
</div>
</body>
</html>
33 changes: 33 additions & 0 deletions app/views/mailers/certificate_mailer/certificate_expiring.text.erb
Original file line number Diff line number Diff line change
@@ -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") %>
50 changes: 50 additions & 0 deletions test/jobs/expire_certificate_reminder_job_test.rb
Original file line number Diff line number Diff line change
@@ -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