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:
LeSim 2024-01-26 09:55:53 +00:00 committed by GitHub
commit d2f8dbdb48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 297 additions and 1 deletions

View 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

View 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

View file

@ -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

View 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 didentification de lAPI » :
= link_to profil_url, profil_url
= render partial: "layouts/mailers/signature"

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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