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/jobs/expire_certificate_reminder_job.rb b/app/jobs/expire_certificate_reminder_job.rb new file mode 100644 index 0000000000..838139a04a --- /dev/null +++ b/app/jobs/expire_certificate_reminder_job.rb @@ -0,0 +1,25 @@ +class ExpireCertificateReminderJob < ApplicationJob + queue_as :default + + def perform + 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 + + private + + 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 + 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/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) 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/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 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 new file mode 100644 index 0000000000..0f1cd087d7 --- /dev/null +++ b/test/jobs/expire_certificate_reminder_job_test.rb @@ -0,0 +1,81 @@ +require 'test_helper' + +class ExpireCertificateReminderJobTest < ActiveJob::TestCase + include ActionMailer::TestHelper + + setup do + ActionMailer::Base.deliveries.clear + @certificate = certificates(:api) + + create_setting_if_not_exists('certificate_reminder_deadline', '30', 'integer', 'certificate') + end + + def test_sends_reminder_for_expiring_certificate + @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 + @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 + + def test_uses_custom_deadline_setting + update_setting('certificate_reminder_deadline', '10') + + @certificate.update(expires_at: 2.weeks.from_now) + + perform_enqueued_jobs do + ExpireCertificateReminderJob.perform_now + end + + assert_emails 0 + + @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