Merge pull request #9931 from demarches-simplifiees/notify_api_token_expiration
ETQ Admin, je suis prévenu par mail lorsque mon jeton arrive a expiration
This commit is contained in:
commit
d2f8dbdb48
9 changed files with 297 additions and 1 deletions
21
app/jobs/cron/send_api_token_expiration_notice_job.rb
Normal file
21
app/jobs/cron/send_api_token_expiration_notice_job.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
class Cron::SendAPITokenExpirationNoticeJob < Cron::CronJob
|
||||
self.schedule_expression = "every day at midnight"
|
||||
|
||||
def perform
|
||||
windows = [
|
||||
1.day,
|
||||
1.week,
|
||||
1.month
|
||||
]
|
||||
|
||||
windows.each do |window|
|
||||
APIToken
|
||||
.with_expiration_notice_to_send_for(window)
|
||||
.find_each do |token|
|
||||
APITokenMailer.expiration(token).deliver_later
|
||||
token.expiration_notices_sent_at << Time.zone.today
|
||||
token.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
18
app/mailers/api_token_mailer.rb
Normal file
18
app/mailers/api_token_mailer.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Preview all emails at http://localhost:3000/rails/mailers/api_token_mailer
|
||||
class APITokenMailer < ApplicationMailer
|
||||
helper MailerHelper
|
||||
|
||||
layout 'mailers/layout'
|
||||
|
||||
def expiration(api_token)
|
||||
@api_token = api_token
|
||||
user = api_token.administrateur.user
|
||||
subject = "Votre jeton d'accès à la plateforme #{APPLICATION_NAME} expire le #{l(@api_token.expires_at, format: :long)}"
|
||||
|
||||
mail(to: user.email, subject:)
|
||||
end
|
||||
|
||||
def self.critical_email?(action_name)
|
||||
false
|
||||
end
|
||||
end
|
|
@ -3,6 +3,26 @@ class APIToken < ApplicationRecord
|
|||
|
||||
belongs_to :administrateur, inverse_of: :api_tokens
|
||||
|
||||
scope :expiring_within, -> (duration) { where(expires_at: Date.today..duration.from_now) }
|
||||
|
||||
scope :without_any_expiration_notice_sent_within, -> (duration) do
|
||||
where.not('(expires_at - (?::interval)) <= some(expiration_notices_sent_at)', duration.iso8601)
|
||||
end
|
||||
|
||||
scope :with_a_bigger_lifetime_than, -> (duration) do
|
||||
where('? < expires_at - created_at', duration.iso8601)
|
||||
end
|
||||
|
||||
scope :with_expiration_notice_to_send_for, -> (duration) do
|
||||
# example for duration = 1.month
|
||||
# take all tokens that expire in the next month
|
||||
# with a lifetime bigger than 1 month
|
||||
# without any expiration notice sent for that period
|
||||
expiring_within(duration)
|
||||
.with_a_bigger_lifetime_than(duration)
|
||||
.without_any_expiration_notice_sent_within(duration)
|
||||
end
|
||||
|
||||
before_save :sanitize_targeted_procedure_ids
|
||||
|
||||
def context
|
||||
|
|
17
app/views/api_token_mailer/expiration.html.haml
Normal file
17
app/views/api_token_mailer/expiration.html.haml
Normal file
|
@ -0,0 +1,17 @@
|
|||
- content_for(:title, "Expiration de votre jeton « #{@api_token.name} »")
|
||||
|
||||
%p
|
||||
Bonjour,
|
||||
|
||||
%p Vous recevez ce courriel car vous êtes le propriétaire du jeton « #{@api_token.name} ».
|
||||
|
||||
%p
|
||||
%strong Ce jeton expirera le #{l(@api_token.expires_at, format: :long)}.
|
||||
%br
|
||||
L'accès à l'API de #{APPLICATION_NAME} sera alors bloqué pour ce jeton.
|
||||
|
||||
%p
|
||||
Pour le renouveler, rendez-vous sur votre page de profil, dans la section « Jetons d’identification de l’API » :
|
||||
= link_to profil_url, profil_url
|
||||
|
||||
= render partial: "layouts/mailers/signature"
|
|
@ -0,0 +1,5 @@
|
|||
class AddExpirationNoticesSentAtColumnToAPIToken < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :api_tokens, :expiration_notices_sent_at, :date, array: true, default: []
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_01_16_155926) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2024_01_23_085909) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -96,6 +96,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_01_16_155926) do
|
|||
t.inet "authorized_networks", default: [], array: true
|
||||
t.datetime "created_at", null: false
|
||||
t.string "encrypted_token", null: false
|
||||
t.date "expiration_notices_sent_at", default: [], array: true
|
||||
t.datetime "last_v1_authenticated_at"
|
||||
t.datetime "last_v2_authenticated_at"
|
||||
t.string "name", null: false
|
||||
|
|
83
spec/jobs/cron/send_api_token_expiration_notice_job_spec.rb
Normal file
83
spec/jobs/cron/send_api_token_expiration_notice_job_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
RSpec.describe Cron::SendAPITokenExpirationNoticeJob, type: :job do
|
||||
describe 'perform' do
|
||||
let(:administrateur) { create(:administrateur) }
|
||||
let!(:token) { APIToken.generate(administrateur).first }
|
||||
let(:mailer_double) { double('mailer', deliver_later: true) }
|
||||
let(:today) { Date.new(2018, 01, 01) }
|
||||
|
||||
def perform_now
|
||||
Cron::SendAPITokenExpirationNoticeJob.perform_now
|
||||
end
|
||||
|
||||
before do
|
||||
travel_to(today)
|
||||
token.update!(created_at: today)
|
||||
allow(APITokenMailer).to receive(:expiration).and_return(mailer_double)
|
||||
end
|
||||
|
||||
context 'when the token does not expire' do
|
||||
before { perform_now }
|
||||
|
||||
it { expect(mailer_double).not_to have_received(:deliver_later) }
|
||||
end
|
||||
|
||||
context 'when the token expires in 6 months' do
|
||||
let(:expires_at) { Date.new(2018, 06, 01) }
|
||||
before do
|
||||
token.update(expires_at:)
|
||||
perform_now
|
||||
end
|
||||
|
||||
it { expect(mailer_double).not_to have_received(:deliver_later) }
|
||||
|
||||
context 'when the token expires less than a month' do
|
||||
before do
|
||||
travel_to(expires_at - 1.month - 1.day)
|
||||
perform_now
|
||||
|
||||
travel_to(expires_at - 1.month)
|
||||
perform_now
|
||||
|
||||
travel_to(expires_at - 1.month + 1.day)
|
||||
perform_now
|
||||
end
|
||||
|
||||
it do
|
||||
expect(mailer_double).to have_received(:deliver_later).once
|
||||
expect(token.reload.expiration_notices_sent_at).to match_array([expires_at - 1.month])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the token expires less than a week' do
|
||||
before do
|
||||
travel_to(expires_at - 1.week)
|
||||
2.times.each { perform_now }
|
||||
end
|
||||
|
||||
it { expect(mailer_double).to have_received(:deliver_later).once }
|
||||
end
|
||||
|
||||
context 'when we simulate the whole sequence' do
|
||||
before do
|
||||
travel_to(expires_at - 1.month)
|
||||
2.times.each { perform_now }
|
||||
|
||||
travel_to(expires_at - 1.week)
|
||||
2.times.each { perform_now }
|
||||
|
||||
travel_to(expires_at - 1.day)
|
||||
2.times.each { perform_now }
|
||||
end
|
||||
|
||||
it do
|
||||
expect(mailer_double).to have_received(:deliver_later).exactly(3).times
|
||||
expect(token.reload.expiration_notices_sent_at).to match_array([
|
||||
expires_at - 1.month,
|
||||
expires_at - 1.week,
|
||||
expires_at - 1.day
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
spec/mailers/previews/api_token_mailer_preview.rb
Normal file
23
spec/mailers/previews/api_token_mailer_preview.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
class APITokenMailerPreview < ActionMailer::Preview
|
||||
def expiration
|
||||
APITokenMailer.expiration(api_token)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_token
|
||||
APIToken.new(
|
||||
administrateur: administrateur,
|
||||
expires_at: 1.week.from_now,
|
||||
name: 'My API token'
|
||||
)
|
||||
end
|
||||
|
||||
def administrateur
|
||||
Administrateur.new(user:)
|
||||
end
|
||||
|
||||
def user
|
||||
User.new(email: 'admin@a.com')
|
||||
end
|
||||
end
|
|
@ -196,4 +196,112 @@ describe APIToken, type: :model do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#expiring_within' do
|
||||
let(:api_token) { APIToken.generate(administrateur).first }
|
||||
|
||||
subject { APIToken.expiring_within(7.days) }
|
||||
|
||||
context 'when the token is not expiring' do
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the token is expiring in the range' do
|
||||
before { api_token.update!(expires_at: 1.day.from_now) }
|
||||
|
||||
it { is_expected.to eq([api_token]) }
|
||||
end
|
||||
|
||||
context 'when the token is not expiring in the range' do
|
||||
before { api_token.update!(expires_at: 8.days.from_now) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the token is expired' do
|
||||
before { api_token.update!(expires_at: 1.day.ago) }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#without_any_expiration_notice_sent_within' do
|
||||
let(:api_token) { APIToken.generate(administrateur).first }
|
||||
let(:today) { Date.new(2018, 01, 01) }
|
||||
let(:expires_at) { Date.new(2018, 06, 01) }
|
||||
|
||||
subject { APIToken.without_any_expiration_notice_sent_within(7.days) }
|
||||
|
||||
before do
|
||||
travel_to(today)
|
||||
api_token.update!(created_at: today, expires_at:)
|
||||
end
|
||||
|
||||
context 'when the token has not been notified' do
|
||||
it { is_expected.to eq([api_token]) }
|
||||
end
|
||||
|
||||
context 'when the token has been notified' do
|
||||
before do
|
||||
api_token.expiration_notices_sent_at << expires_at - 7.days
|
||||
api_token.save!
|
||||
end
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the token has been notified outside the window' do
|
||||
before do
|
||||
api_token.expiration_notices_sent_at << expires_at - 8.days
|
||||
api_token.save!
|
||||
end
|
||||
|
||||
it { is_expected.to eq([api_token]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#with_expiration_notice_to_send_for' do
|
||||
let(:api_token) { APIToken.generate(administrateur).first }
|
||||
let(:duration) { 7.days }
|
||||
|
||||
subject do
|
||||
APIToken.with_expiration_notice_to_send_for(duration)
|
||||
end
|
||||
|
||||
context 'when the token is expiring in the range' do
|
||||
before { api_token.update!(expires_at: 1.day.from_now, created_at:) }
|
||||
|
||||
let(:created_at) { 1.year.ago }
|
||||
|
||||
it do
|
||||
is_expected.to eq([api_token])
|
||||
end
|
||||
|
||||
context 'when the token has been created within the time frame' do
|
||||
let(:created_at) { 2.days.ago }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the token has already been notified' do
|
||||
before do
|
||||
api_token.expiration_notices_sent_at << 1.day.ago
|
||||
api_token.save!
|
||||
end
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'when the token has already been notified for another window' do
|
||||
before do
|
||||
api_token.expiration_notices_sent_at << 1.month.ago
|
||||
api_token.save!
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([api_token])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue