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
11 changes: 11 additions & 0 deletions app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ def user_acceptance(user)
mail(to: user.email, subject: I18n.t("user_mailer.user_acceptance.subject"))
end

def inactive_account_warning(user, disable_date)
@user = user
@disable_date = disable_date
mail(to: user.email, subject: I18n.t("user_mailer.inactive_account_warning.subject", disable_date: I18n.l(disable_date)))
end

def account_deactivated_for_inactivity(user)
@user = user
mail(to: user.email, subject: I18n.t("user_mailer.account_deactivated_for_inactivity.subject"))
end

private

def generate_reset_url(user)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<p>
<%= t("mailers.greeting", name: @user.display_name, fallback: true) %>
</p>

<p>
<%= t(".message") %>
</p>

<p>
<%= t(".contact") %>
</p>

<p>
<%= t("mailers.salutation") %> <br />
<%= t("mailers.signature") %>
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%= t("mailers.greeting", name: @user.display_name, fallback: true) %>

<%= t(".message") %>

<%= t(".contact") %>

<%= t("mailers.salutation") %>
<%= t("mailers.signature") %>
20 changes: 20 additions & 0 deletions app/views/mailers/user_mailer/inactive_account_warning.html.mjml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<p>
<%= t("mailers.greeting", name: @user.display_name, fallback: true) %>
</p>

<p>
<%= t(".message", disable_date: l(@disable_date)) %>
</p>

<p>
<%= t(".cta", link: link_to(ENV["FRONTEND_URL"], ENV["FRONTEND_URL"], target: "_blank")).html_safe %>
</p>

<p>
<%= t("mailers.contact_us") %>
</p>

<p>
<%= t("mailers.salutation") %> <br />
<%= t("mailers.signature") %>
</p>
10 changes: 10 additions & 0 deletions app/views/mailers/user_mailer/inactive_account_warning.text.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= t("mailers.greeting", name: @user.display_name, fallback: true) %>

<%= t(".message", disable_date: l(@disable_date)) %>

<%= t(".cta", link: ENV["FRONTEND_URL"]) %>

<%= t("mailers.contact_us") %>

<%= t("mailers.salutation") %>
<%= t("mailers.signature") %>
8 changes: 8 additions & 0 deletions config/locales/mailers.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ en:
message: Please click on the link to reset your password to connect to the Open Timber Portal
link_expiration: The above link will expire in 6 hours. If you made multiple requests to reset your password, please make sure to click on the link in the most recent e-mail.
outro: If you didn't request this password reset, please ignore this email. Your password will not change until you access the link above and create a new password.
inactive_account_warning:
subject: "Open Timber Portal account inactivity notice - disable date %{disable_date}"
message: "Your account has not been used for 18 months. Please log in soon to keep it active. If there is no login activity, your account will be disabled on %{disable_date}."
cta: "Log in here: %{link}"
account_deactivated_for_inactivity:
subject: "Your Open Timber Portal account has been deactivated"
message: "Because your account has not been used for 2 years, it is now deactivated."
contact: "If you need help or think this is an error, please contact the OTP team."
observation_mailer:
observation_created:
subject: Observation %{id} was successfully created
Expand Down
8 changes: 8 additions & 0 deletions config/locales/mailers.es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ es:
message: "Haga clic en el enlace para restablecer su contraseña y conectarse al Open Timber Portal"
link_expiration: "El enlace anterior expirará en 6 horas. Si realizó múltiples solicitudes para restablecer su contraseña, asegúrese de hacer clic en el enlace del correo electrónico más reciente."
outro: "Si no solicitó este restablecimiento de contraseña, ignore este correo electrónico. Su contraseña no cambiará hasta que acceda al enlace anterior y cree una nueva contraseña."
inactive_account_warning:
subject: "Aviso de inactividad de su cuenta de Open Timber Portal - fecha de desactivacion %{disable_date}"
message: "Su cuenta no se ha utilizado durante 18 meses. Inicie sesion pronto para mantenerla activa. Si no hay actividad de inicio de sesion, su cuenta se desactivara el %{disable_date}."
cta: "Inicie sesion aqui: %{link}"
account_deactivated_for_inactivity:
subject: "Su cuenta de Open Timber Portal ha sido desactivada"
message: "Debido a que su cuenta no se ha utilizado en 2 anos, ahora esta desactivada."
contact: "Si necesita ayuda o cree que esto es un error, contacte al equipo de OTP."
observation_mailer:
observation_created:
subject: "La observación %{id} se creó exitosamente"
Expand Down
8 changes: 8 additions & 0 deletions config/locales/mailers.fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ fr:
message: Veuillez cliquer sur le lien afin de réinitialiser votre mot de passe pour vous connecter à l’Open Timber Portal
link_expiration: Le lien ci-dessus expire après 6 heures. Si vous avez fait plusieurs demandes de réinitialisation, veuillez vous assurer de cliquer sur le lien qui figure dans le mail le plus récent.
outro: Si vous n’avez pas fait cette demande de réinitialisation, veuillez ignorer cet e-mail. Votre mot de passe ne changera pas tant que vous n'aurez pas cliqué sur le lien ci-dessus et créé un nouveau mot de passe.
inactive_account_warning:
subject: "Avis d'inactivite du compte Open Timber Portal - date de desactivation %{disable_date}"
message: "Votre compte n'a pas ete utilise depuis 18 mois. Connectez-vous bientot pour le garder actif. Sans nouvelle connexion, votre compte sera desactive le %{disable_date}."
cta: "Connectez-vous ici: %{link}"
account_deactivated_for_inactivity:
subject: "Votre compte Open Timber Portal a ete desactive"
message: "Parce que votre compte n'a pas ete utilise depuis 2 ans, il est maintenant desactive."
contact: "Si vous avez besoin d'aide ou pensez qu'il s'agit d'une erreur, contactez l'equipe OTP."
observation_mailer:
observation_created:
subject: L’observation %{id} a été créée
Expand Down
1 change: 1 addition & 0 deletions config/schedule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
rake "scheduler:set_active_fmu_operator", check_in: "update-fmus"
rake "scheduler:generate_documents_stats", check_in: "generate-documents-stats"
rake "scheduler:generate_observation_reports_stats", check_in: "generate-observation-reports-stats"
rake "scheduler:deactivate_inactive_users", check_in: "deactivate-inactive-users"
end

every 1.hour do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLastInactivityWarningSentAtToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :last_inactivity_warning_sent_at, :datetime
end
end
3 changes: 2 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.2].define(version: 2026_02_23_113655) do
ActiveRecord::Schema[7.2].define(version: 2026_04_30_153500) do
create_schema "tiger"
create_schema "tiger_data"
create_schema "topology"
Expand Down Expand Up @@ -1122,6 +1122,7 @@
t.string "last_name"
t.boolean "organization_account", default: false, null: false
t.boolean "should_change_password", default: false, null: false
t.datetime "last_inactivity_warning_sent_at"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["name"], name: "index_users_on_name"
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
Expand Down
43 changes: 43 additions & 0 deletions lib/tasks/scheduler.rake
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,47 @@ namespace :scheduler do
Rails.logger.info "Sent quarterly newsletters to operators. It took #{time} ms."
Rails.logger.info "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
end

desc "Warn and deactivate inactive users"
task deactivate_inactive_users: :environment do
Rails.logger.info "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
Rails.logger.info "Going to process inactive users at: #{Time.zone.now.strftime("%d/%m/%Y %H:%M")}"

warning_threshold = 18.months.ago.end_of_day
warning_cooldown = 1.month.ago.end_of_day
deactivation_threshold = 2.years.ago.end_of_day

warned_users = 0
deactivated_users = 0

time = Benchmark.ms do
warning_users = User.where(is_active: true)
.where("COALESCE(last_sign_in_at, created_at) <= ?", warning_threshold)
.where("COALESCE(last_sign_in_at, created_at) > ?", deactivation_threshold)
.where("last_inactivity_warning_sent_at IS NULL OR last_inactivity_warning_sent_at <= ?", warning_cooldown)

warning_users.find_each do |user|
disable_date = (user.last_sign_in_at || user.created_at).to_date + 2.years
I18n.with_locale(user.locale.presence || I18n.default_locale) do
UserMailer.inactive_account_warning(user, disable_date).deliver_now
end
user.update!(last_inactivity_warning_sent_at: Time.zone.now)
warned_users += 1
end

users_to_deactivate = User.where(is_active: true)
.where("COALESCE(last_sign_in_at, created_at) <= ?", deactivation_threshold)

users_to_deactivate.find_each do |user|
user.update!(is_active: false, deactivated_at: Time.zone.now)
I18n.with_locale(user.locale.presence || I18n.default_locale) do
UserMailer.account_deactivated_for_inactivity(user).deliver_now
end
deactivated_users += 1
end
end

Rails.logger.info "Processed inactive users. Warned=#{warned_users}, Deactivated=#{deactivated_users}. It took #{time} ms."
Rails.logger.info "::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::"
end
end
70 changes: 70 additions & 0 deletions spec/lib/tasks/deactivate_inactive_users_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require "rails_helper"

Rails.application.load_tasks if Rake::Task.tasks.empty?

describe "deactivate_inactive_users" do
include ActiveSupport::Testing::TimeHelpers

after(:each) do
Rake::Task["scheduler:deactivate_inactive_users"].reenable
ActionMailer::Base.deliveries.clear
end

subject(:run_task) { Rake::Task["scheduler:deactivate_inactive_users"].invoke }

around do |example|
travel_to(Time.zone.parse("2026-04-30 10:00:00")) { example.run }
end

it "sends warning emails to users inactive for at least 18 months" do
warned_user = create(:operator_user, last_sign_in_at: 18.months.ago - 1.day)
create(:operator_user, last_sign_in_at: 17.months.ago)

expect { run_task }.to change { ActionMailer::Base.deliveries.count }.by(1)

mail = ActionMailer::Base.deliveries.first
expect(mail.to).to eq([warned_user.email])
expect(mail.body.encoded).to include(I18n.l(warned_user.last_sign_in_at.to_date + 2.years))
end

it "does not send warning email again within one month" do
create(
:operator_user,
last_sign_in_at: 19.months.ago,
last_inactivity_warning_sent_at: 20.days.ago
)

expect { run_task }.not_to change { ActionMailer::Base.deliveries.count }
end

it "sends warning email again when one month has passed since last warning" do
warned_user = create(
:operator_user,
last_sign_in_at: 19.months.ago,
last_inactivity_warning_sent_at: 1.month.ago - 1.day
)

expect { run_task }.to change { ActionMailer::Base.deliveries.count }.by(1)
expect(warned_user.reload.last_inactivity_warning_sent_at).to be_present
end

it "deactivates users inactive for 2 years or more" do
old_user = create(:operator_user, is_active: true, last_sign_in_at: 2.years.ago - 1.day, deactivated_at: nil)
active_user = create(:operator_user, is_active: true, last_sign_in_at: 1.year.ago)

expect { run_task }.to change { old_user.reload.is_active }.from(true).to(false)
expect(active_user.reload.is_active).to eq(true)

expect(old_user.reload.deactivated_at).to be_present
expect(ActionMailer::Base.deliveries.last.to).to eq([old_user.email])
expect(ActionMailer::Base.deliveries.last.subject).to eq(I18n.t("user_mailer.account_deactivated_for_inactivity.subject"))
end

it "uses created_at when user has never logged in" do
never_logged_user = create(:operator_user, last_sign_in_at: nil, created_at: 18.months.ago)

expect { run_task }.to change { ActionMailer::Base.deliveries.count }.by(1)

expect(ActionMailer::Base.deliveries.first.to).to eq([never_logged_user.email])
end
end
27 changes: 27 additions & 0 deletions spec/mailers/user_mailer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,31 @@
expect(mail.body.encoded).to match(I18n.t("user_mailer.forgotten_password.message"))
end
end

describe "inactive_account_warning" do
let(:disable_date) { Date.new(2028, 1, 1) }
let(:mail) { UserMailer.inactive_account_warning(user, disable_date) }

it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.inactive_account_warning.subject", disable_date: I18n.l(disable_date)))
expect(mail.to).to eq([user.email])
end

it "renders the body" do
expect(mail.body.encoded).to match(I18n.t("user_mailer.inactive_account_warning.message", disable_date: I18n.l(disable_date)))
end
end

describe "account_deactivated_for_inactivity" do
let(:mail) { UserMailer.account_deactivated_for_inactivity(user) }

it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.account_deactivated_for_inactivity.subject"))
expect(mail.to).to eq([user.email])
end

it "renders the body" do
expect(mail.body.encoded).to match(I18n.t("user_mailer.account_deactivated_for_inactivity.message"))
end
end
end
Loading