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
1 change: 1 addition & 0 deletions app/controllers/admin/settings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions app/jobs/expire_certificate_reminder_job.rb
Original file line number Diff line number Diff line change
@@ -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
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
9 changes: 9 additions & 0 deletions app/views/admin/settings/index.haml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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") %>
1 change: 1 addition & 0 deletions config/locales/admin/settings.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ en:
billing: Billing
contacts: Contacts
save_btn: Save
certificate: Certificate

create:
saved: Settings have been successfully updated
5 changes: 5 additions & 0 deletions db/migrate/20250910112808_add_reminder_sent_to_certificate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddReminderSentToCertificate < ActiveRecord::Migration[6.1]
def change
add_column :certificates, :reminder_sent, :boolean, default: false
end
end
Original file line number Diff line number Diff line change
@@ -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
14 changes: 10 additions & 4 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
);


Expand Down Expand Up @@ -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[]
);


Expand Down Expand Up @@ -5777,7 +5779,11 @@ INSERT INTO "schema_migrations" (version) VALUES
('20241206085817'),
('20250204094550'),
('20250219102811'),
('20250310133151'),
('20250313122119'),
('20250319104749'),
('20250310133151'),
('20250314133357');
('20250627084536'),
('20250910112808'),
('20250910113941');


81 changes: 81 additions & 0 deletions test/jobs/expire_certificate_reminder_job_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading