From e23bcb1ae9cff5a9e0d6de20ac7c481be4122bff Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Wed, 10 Sep 2025 14:08:51 +0300 Subject: [PATCH 1/6] added expired certificate reminder --- app/jobs/expire_certificate_reminder_job.rb | 23 +++++ app/mailers/certificate_mailer.rb | 6 ++ .../certificate_expiring.html.erb | 97 +++++++++++++++++++ .../certificate_expiring.text.erb | 33 +++++++ .../expire_certificate_reminder_job_test.rb | 50 ++++++++++ 5 files changed, 209 insertions(+) create mode 100644 app/jobs/expire_certificate_reminder_job.rb create mode 100644 app/views/mailers/certificate_mailer/certificate_expiring.html.erb create mode 100644 app/views/mailers/certificate_mailer/certificate_expiring.text.erb create mode 100644 test/jobs/expire_certificate_reminder_job_test.rb 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/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 @@ + + + + + + Certificate Expiring + + + +
+

Certificate Expiring Soon

+

Your certificate is approaching its expiration date and requires attention.

+
+ +
+ ⚠️ Action Required: Your certificate will expire soon. Please renew it to avoid service interruption. +
+ +
+

Certificate Details:

+ +
+ +

What you need to do:

+
    +
  1. Generate a new Certificate Signing Request (CSR)
  2. +
  3. Submit the CSR through your registrar interface
  4. +
  5. Install the new certificate before the current one expires
  6. +
+ +

+ 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 From dfcc3ce21fe8ce0a8e0c79610da3c34445f587e4 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Wed, 10 Sep 2025 14:57:34 +0300 Subject: [PATCH 2/6] added customization deadline set for certificate expire reminder --- app/jobs/expire_certificate_reminder_job.rb | 8 ++-- ...112808_add_reminder_sent_to_certificate.rb | 5 +++ ...rtificate_reminder_deadline_to_settings.rb | 18 ++++++++ db/structure.sql | 14 ++++-- .../expire_certificate_reminder_job_test.rb | 43 +++++++++++++++++-- 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 db/migrate/20250910112808_add_reminder_sent_to_certificate.rb create mode 100644 db/migrate/20250910113941_add_certificate_reminder_deadline_to_settings.rb diff --git a/app/jobs/expire_certificate_reminder_job.rb b/app/jobs/expire_certificate_reminder_job.rb index 196c53d1a8..60df21beea 100644 --- a/app/jobs/expire_certificate_reminder_job.rb +++ b/app/jobs/expire_certificate_reminder_job.rb @@ -1,10 +1,11 @@ class ExpireCertificateReminderJob < ApplicationJob queue_as :default - - DEADLINE = 1.month def perform - Certificate.where('expires_at < ?', DEADLINE.from_now).each do |certificate| + deadline_days = Setting.certificate_reminder_deadline || 30 + deadline = deadline_days.days.from_now + + Certificate.where('expires_at < ?', deadline).where(reminder_sent: false).each do |certificate| send_reminder(certificate) end end @@ -19,5 +20,6 @@ def send_reminder(certificate) def send_email(registrar, certificate) CertificateMailer.certificate_expiring(email: registrar.email, certificate: certificate).deliver_now + certificate.update(reminder_sent: true) end end diff --git a/db/migrate/20250910112808_add_reminder_sent_to_certificate.rb b/db/migrate/20250910112808_add_reminder_sent_to_certificate.rb new file mode 100644 index 0000000000..a84bbde3a3 --- /dev/null +++ b/db/migrate/20250910112808_add_reminder_sent_to_certificate.rb @@ -0,0 +1,5 @@ +class AddReminderSentToCertificate < ActiveRecord::Migration[6.1] + def change + add_column :certificates, :reminder_sent, :boolean, default: false + end +end diff --git a/db/migrate/20250910113941_add_certificate_reminder_deadline_to_settings.rb b/db/migrate/20250910113941_add_certificate_reminder_deadline_to_settings.rb new file mode 100644 index 0000000000..a0292e0b70 --- /dev/null +++ b/db/migrate/20250910113941_add_certificate_reminder_deadline_to_settings.rb @@ -0,0 +1,18 @@ +class AddCertificateReminderDeadlineToSettings < ActiveRecord::Migration[6.1] + def up + unless SettingEntry.exists?(code: 'certificate_reminder_deadline') + SettingEntry.create!( + code: 'certificate_reminder_deadline', + value: '30', + format: 'integer', + group: 'certificate' + ) + else + puts "SettingEntry certificate_reminder_deadline already exists" + end + end + + def down + SettingEntry.where(code: 'certificate_reminder_deadline').destroy_all + end +end diff --git a/db/structure.sql b/db/structure.sql index 4502faae08..3e0bff03d0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -629,7 +629,8 @@ CREATE TABLE public.certificates ( serial character varying, revoked_at timestamp without time zone, revoked_reason integer, - p12_password character varying + p12_password character varying, + reminder_sent boolean DEFAULT false ); @@ -732,7 +733,8 @@ CREATE TABLE public.contacts ( company_register_status character varying, ident_request_sent_at timestamp without time zone, verified_at timestamp without time zone, - verification_id character varying + verification_id character varying, + system_disclosed_attributes character varying[] DEFAULT '{}'::character varying[] ); @@ -5777,7 +5779,11 @@ INSERT INTO "schema_migrations" (version) VALUES ('20241206085817'), ('20250204094550'), ('20250219102811'), +('20250310133151'), ('20250313122119'), ('20250319104749'), -('20250310133151'), -('20250314133357'); +('20250627084536'), +('20250910112808'), +('20250910113941'); + + diff --git a/test/jobs/expire_certificate_reminder_job_test.rb b/test/jobs/expire_certificate_reminder_job_test.rb index 59df68afc0..036ff660cc 100644 --- a/test/jobs/expire_certificate_reminder_job_test.rb +++ b/test/jobs/expire_certificate_reminder_job_test.rb @@ -6,10 +6,12 @@ class ExpireCertificateReminderJobTest < ActiveJob::TestCase setup do ActionMailer::Base.deliveries.clear @certificate = certificates(:api) + + # Устанавливаем настройку по умолчанию для тестов (30 дней) + create_setting_if_not_exists('certificate_reminder_deadline', '30', 'integer', 'certificate') end def test_sends_reminder_for_expiring_certificate - # Устанавливаем дату истечения на 2 недели от текущего времени (меньше месяца) @certificate.update(expires_at: 2.weeks.from_now) perform_enqueued_jobs do @@ -18,14 +20,12 @@ def test_sends_reminder_for_expiring_certificate 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 @@ -36,7 +36,6 @@ def test_does_not_send_reminder_for_certificate_expiring_later 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) @@ -47,4 +46,40 @@ def test_sends_reminder_for_multiple_expiring_certificates assert_emails 2 end + + def test_uses_custom_deadline_setting + # Изменяем настройку на 10 дней + update_setting('certificate_reminder_deadline', '10') + + # Сертификат истекает через 2 недели (больше 10 дней) + @certificate.update(expires_at: 2.weeks.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 0 + + # Сертификат истекает через 5 дней (меньше 10 дней) + @certificate.update(expires_at: 5.days.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 1 + end + + private + + def create_setting_if_not_exists(code, value, format, group) + unless SettingEntry.exists?(code: code) + SettingEntry.create!(code: code, value: value, format: format, group: group) + end + end + + def update_setting(code, value) + setting = SettingEntry.find_by(code: code) + setting.update!(value: value) if setting + end end From 11c544996991379097e8fb8709b423d15e00a862 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Thu, 11 Sep 2025 12:08:35 +0300 Subject: [PATCH 3/6] added certificate expire day reminding into the admin panel --- app/controllers/admin/settings_controller.rb | 1 + app/views/admin/settings/index.haml | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 1ebf2946d8..5ec7a91674 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -10,6 +10,7 @@ def index .where.not(code: 'default_language') @billing_settings = SettingEntry.with_group('billing') @contacts_settings = SettingEntry.with_group('contacts') + @certificate_settings = SettingEntry.with_group('certificate') end def create diff --git a/app/views/admin/settings/index.haml b/app/views/admin/settings/index.haml index c9a272a295..b9ba72f448 100644 --- a/app/views/admin/settings/index.haml +++ b/app/views/admin/settings/index.haml @@ -54,6 +54,15 @@ - @contacts_settings.each do |setting| = render 'setting_row', setting: setting + .panel.panel-default + .panel-heading + = t('.certificate') + .table-responsive + %table.table.table-hover.table-bordered.table-condensed + %tbody + - @certificate_settings.each do |setting| + = render 'setting_row', setting: setting + .row .col-md-12.text-right = submit_tag(t('.save_btn'), class: 'btn btn-success', name: nil) From 2675d594e846de9904977a451f72bf96665cb390 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Thu, 11 Sep 2025 12:16:15 +0300 Subject: [PATCH 4/6] added missing local title --- config/locales/admin/settings.en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/admin/settings.en.yml b/config/locales/admin/settings.en.yml index bb042db013..21dbbf4fa0 100644 --- a/config/locales/admin/settings.en.yml +++ b/config/locales/admin/settings.en.yml @@ -9,6 +9,7 @@ en: billing: Billing contacts: Contacts save_btn: Save + certificate: Certificate create: saved: Settings have been successfully updated From 8b75ed48d7b051eb38fe40355b2ec219dcfc96b0 Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Thu, 11 Sep 2025 12:26:27 +0300 Subject: [PATCH 5/6] refactor --- app/jobs/expire_certificate_reminder_job.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/expire_certificate_reminder_job.rb b/app/jobs/expire_certificate_reminder_job.rb index 60df21beea..838139a04a 100644 --- a/app/jobs/expire_certificate_reminder_job.rb +++ b/app/jobs/expire_certificate_reminder_job.rb @@ -16,10 +16,10 @@ def send_reminder(certificate) registrar = certificate.api_user.registrar send_email(registrar, certificate) + certificate.update(reminder_sent: true) end def send_email(registrar, certificate) CertificateMailer.certificate_expiring(email: registrar.email, certificate: certificate).deliver_now - certificate.update(reminder_sent: true) end end From 8f8ecfa1b36b3763a9b27795fb018223ef08a6cd Mon Sep 17 00:00:00 2001 From: oleghasjanov Date: Wed, 1 Oct 2025 10:24:19 +0300 Subject: [PATCH 6/6] remove useless comments --- test/jobs/expire_certificate_reminder_job_test.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/jobs/expire_certificate_reminder_job_test.rb b/test/jobs/expire_certificate_reminder_job_test.rb index 036ff660cc..0f1cd087d7 100644 --- a/test/jobs/expire_certificate_reminder_job_test.rb +++ b/test/jobs/expire_certificate_reminder_job_test.rb @@ -7,7 +7,6 @@ class ExpireCertificateReminderJobTest < ActiveJob::TestCase ActionMailer::Base.deliveries.clear @certificate = certificates(:api) - # Устанавливаем настройку по умолчанию для тестов (30 дней) create_setting_if_not_exists('certificate_reminder_deadline', '30', 'integer', 'certificate') end @@ -48,10 +47,8 @@ def test_sends_reminder_for_multiple_expiring_certificates end def test_uses_custom_deadline_setting - # Изменяем настройку на 10 дней update_setting('certificate_reminder_deadline', '10') - # Сертификат истекает через 2 недели (больше 10 дней) @certificate.update(expires_at: 2.weeks.from_now) perform_enqueued_jobs do @@ -60,7 +57,6 @@ def test_uses_custom_deadline_setting assert_emails 0 - # Сертификат истекает через 5 дней (меньше 10 дней) @certificate.update(expires_at: 5.days.from_now) perform_enqueued_jobs do