From e49d5b2a3576b9959f015e4a8f57eedd4be8ac70 Mon Sep 17 00:00:00 2001 From: Saverio Trioni <476895+rewritten@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:24:06 +0100 Subject: [PATCH 1/4] feat(admin): allow admins to confirm users and set passwords Admins can now create users that are already confirmed (bypassing email confirmation) and optionally set a password at creation time. Existing unconfirmed users can be confirmed via a dedicated action button on their show page. Confirmed-at timestamp is also shown in the user index. Closes #821 Co-Authored-By: Claude Sonnet 4.6 --- app/admin/user.rb | 35 ++++++++++++++++++++++++++++++++++- app/models/user.rb | 1 + config/locales/ca.yml | 3 +++ config/locales/en.yml | 3 +++ config/locales/es.yml | 3 +++ config/locales/eu.yml | 3 +++ config/locales/fr.yml | 3 +++ config/locales/gl.yml | 3 +++ config/locales/ja.yml | 3 +++ config/locales/pt-BR.yml | 3 +++ 10 files changed, 59 insertions(+), 1 deletion(-) diff --git a/app/admin/user.rb b/app/admin/user.rb index f34aed6c..4184a311 100644 --- a/app/admin/user.rb +++ b/app/admin/user.rb @@ -5,6 +5,14 @@ link_to I18n.t("active_admin.users.upload_from_csv"), action: "upload_csv" end + action_item :confirm, only: :show do + unless user.confirmed? + link_to I18n.t("active_admin.users.confirm_user"), + confirm_admin_user_path(user), + method: :put + end + end + collection_action :upload_csv do render "admin/csv/upload_csv" end @@ -16,6 +24,11 @@ redirect_to action: :index end + member_action :confirm, method: :put do + resource.confirm! + redirect_to admin_user_path(resource), notice: I18n.t("active_admin.users.confirmed_notice") + end + scope :all scope :without_memberships @@ -32,6 +45,7 @@ column :posts do |u| u.posts.count end + column :confirmed_at column :created_at actions end @@ -53,6 +67,9 @@ f.input :postcode f.input :gender, as: :select, collection: User::GENDERS f.input :locale, as: :select, collection: I18n.available_locales + f.input :password, required: false, input_html: { autocomplete: "new-password" } + f.input :password_confirmation, required: false, input_html: { autocomplete: "new-password" } + f.input :confirm_immediately, as: :boolean if f.object.new_record? end f.inputs "Memberships" do f.has_many :members, allow_destroy: true do |m| @@ -91,6 +108,22 @@ end end - permit_params :username, :email, :phone, :postcode, :gender, :locale, + permit_params :username, :email, :phone, :postcode, :gender, :locale, :confirm_immediately, + :password, :password_confirmation, members_attributes: [:id, :organization_id, :active, :manager, :_destroy] + + controller do + def create_resource(obj) + obj.skip_confirmation! if obj.confirm_immediately + super + end + + def update_resource(obj, attributes) + if attributes.first[:password].blank? + attributes.first.delete(:password) + attributes.first.delete(:password_confirmation) + end + super + end + end end diff --git a/app/models/user.rb b/app/models/user.rb index d503083a..2254d313 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ class User < ApplicationRecord attr_accessor :empty_email attr_accessor :from_signup + attr_accessor :confirm_immediately has_one_attached :avatar diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 6a2c6868..e693a026 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -2,6 +2,8 @@ ca: active_admin: users: organization: Banc de temps + confirm_user: Confirmar usuari + confirmed_notice: Usuari confirmat correctament upload_csv: Fitxer upload_from_csv: Importar CSV activerecord: @@ -70,6 +72,7 @@ ca: registration_date: Data d'alta registration_number: Codi d'usuari superadmin: SuperAdministrador del Sistema + confirm_immediately: Confirmar immediatament unconfirmed_email: Correu sense confirmar updated_at: Actualitzat username: Nom diff --git a/config/locales/en.yml b/config/locales/en.yml index 1d26d73f..551b8883 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -2,6 +2,8 @@ en: active_admin: users: organization: Time bank + confirm_user: Confirm user + confirmed_notice: User confirmed successfully upload_csv: File upload_from_csv: Upload CSV activerecord: @@ -70,6 +72,7 @@ en: registration_date: Registration date registration_number: User code superadmin: System Administrator + confirm_immediately: Confirm immediately unconfirmed_email: Unconfirmed Email updated_at: Updated username: Name diff --git a/config/locales/es.yml b/config/locales/es.yml index 3490a28d..14fb665b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -2,6 +2,8 @@ es: active_admin: users: organization: Banco de tiempo + confirm_user: Confirmar usuario + confirmed_notice: Usuario confirmado correctamente upload_csv: Fichero upload_from_csv: Importar CSV activerecord: @@ -70,6 +72,7 @@ es: registration_date: Fecha de alta registration_number: Código de usuario superadmin: Administrador de sistema + confirm_immediately: Confirmar inmediatamente unconfirmed_email: Correo sin confirmar updated_at: Actualizado username: Nombre diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 506b3398..cf2922a9 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -2,6 +2,8 @@ eu: active_admin: users: organization: Denbora Bankua + confirm_user: Erabiltzailea baieztatu + confirmed_notice: Erabiltzailea berretsita upload_csv: Fitxategia upload_from_csv: Igo CSVa activerecord: @@ -70,6 +72,7 @@ eu: registration_date: Erregistratze-da registration_number: Erabiltzaile kodea superadmin: Sistema administratzailea + confirm_immediately: Berehala baieztatu unconfirmed_email: Baieztatu gabeko eposta updated_at: Eguneratua username: Izena diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 9e9bf345..8a5779b7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -2,6 +2,8 @@ fr: active_admin: users: organization: Banque de temps + confirm_user: "Confirmer l'utilisateur" + confirmed_notice: "Utilisateur confirmé avec succès" upload_csv: Fichier upload_from_csv: Uploader un CSV activerecord: @@ -70,6 +72,7 @@ fr: registration_date: Date d'inscription registration_number: Numéro d'inscription superadmin: Administrateur système + confirm_immediately: Confirmer immédiatement unconfirmed_email: Adresse email non-confirmée updated_at: Mis·e à jour le username: Nom diff --git a/config/locales/gl.yml b/config/locales/gl.yml index e5affa6b..776de46c 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -2,6 +2,8 @@ gl: active_admin: users: organization: Banco de tempo + confirm_user: Confirmar usuario + confirmed_notice: Usuario confirmado correctamente upload_csv: Ficheiro upload_from_csv: Importar CSV activerecord: @@ -70,6 +72,7 @@ gl: registration_date: Data de rexistro registration_number: Código de persoa usuaria superadmin: Persoa dministradora do sistema + confirm_immediately: Confirmar inmediatamente unconfirmed_email: Correo electrónico non confirmado updated_at: Actualizado username: Nome diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 9494d415..3f06c06d 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -2,6 +2,8 @@ ja: active_admin: users: organization: タイムバンク + confirm_user: ユーザーを確認 + confirmed_notice: ユーザーが確認されました upload_csv: ファイル upload_from_csv: CSVをインポート activerecord: @@ -70,6 +72,7 @@ ja: registration_date: 登録日 registration_number: ユーザー番号 superadmin: システム管理者 + confirm_immediately: すぐに確認する unconfirmed_email: 未確認メールアドレス updated_at: 更新 username: 名前 diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index c575806b..5da85f8f 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -2,6 +2,8 @@ pt-BR: active_admin: users: organization: Banco de Tempo + confirm_user: Confirmar usuário + confirmed_notice: Usuário confirmado com sucesso upload_csv: Arquivo upload_from_csv: Importar CSV activerecord: @@ -70,6 +72,7 @@ pt-BR: registration_date: Data de ingresso registration_number: Código do usuário superadmin: Administrador do sistema + confirm_immediately: Confirmar imediatamente unconfirmed_email: E-mail sem confirmação updated_at: Atualizado username: Nome From 656a83b61207f00c0d01750ef84ce2b771dc9d16 Mon Sep 17 00:00:00 2001 From: Saverio Trioni <476895+rewritten@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:10:33 +0100 Subject: [PATCH 2/4] Update app/models/user.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 2254d313..37906285 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,7 +22,7 @@ class User < ApplicationRecord attr_accessor :empty_email attr_accessor :from_signup - attr_accessor :confirm_immediately + attribute :confirm_immediately, :boolean has_one_attached :avatar From f57fa9b630feb8b3c3d60cd99645123ea010cacf Mon Sep 17 00:00:00 2001 From: Saverio Trioni <476895+rewritten@users.noreply.github.com> Date: Sun, 5 Apr 2026 15:52:21 +0200 Subject: [PATCH 3/4] Fix controller + add tests --- app/admin/user.rb | 7 ++- spec/admin/users_controller_spec.rb | 79 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 spec/admin/users_controller_spec.rb diff --git a/app/admin/user.rb b/app/admin/user.rb index 4184a311..0ed7ce88 100644 --- a/app/admin/user.rb +++ b/app/admin/user.rb @@ -25,8 +25,11 @@ end member_action :confirm, method: :put do - resource.confirm! - redirect_to admin_user_path(resource), notice: I18n.t("active_admin.users.confirmed_notice") + if resource.confirm + redirect_to admin_user_path(resource), notice: I18n.t("active_admin.users.confirmed_notice") + else + redirect_to admin_user_path(resource), alert: resource.errors.full_messages.to_sentence + end end scope :all diff --git a/spec/admin/users_controller_spec.rb b/spec/admin/users_controller_spec.rb new file mode 100644 index 00000000..f8497150 --- /dev/null +++ b/spec/admin/users_controller_spec.rb @@ -0,0 +1,79 @@ +RSpec.describe Admin::UsersController, type: :controller do + let(:organization) { Fabricate(:organization) } + let(:member) { Fabricate(:member, organization: organization) } + let(:user) { member.user } + + before do + login(user) + allow(controller).to receive(:authenticate_superuser!).and_return(true) + end + + describe "PUT #confirm" do + context "when the user is unconfirmed" do + let(:unconfirmed_user) { Fabricate(:user, confirmed_at: nil) } + + it "confirms the user and redirects with notice" do + put :confirm, params: { id: unconfirmed_user.id } + + expect(unconfirmed_user.reload.confirmed?).to be true + expect(response).to redirect_to(admin_user_path(unconfirmed_user)) + expect(flash[:notice]).to eq(I18n.t("active_admin.users.confirmed_notice")) + end + end + + context "when the user is already confirmed" do + it "redirects with an alert" do + put :confirm, params: { id: user.id } + + expect(response).to redirect_to(admin_user_path(user)) + expect(flash[:alert]).to be_present + end + end + end + + describe "POST #create" do + let(:valid_params) do + { username: "newuser", email: "new@example.com", locale: "en" } + end + + context "with confirm_immediately checked" do + it "creates a confirmed user" do + post :create, params: { user: valid_params.merge(confirm_immediately: "1") } + + created_user = User.find_by(email: "new@example.com") + expect(created_user).to be_confirmed + end + end + + context "without confirm_immediately checked" do + it "creates an unconfirmed user" do + post :create, params: { user: valid_params.merge(confirm_immediately: "0") } + + created_user = User.find_by(email: "new@example.com") + expect(created_user).not_to be_confirmed + end + end + end + + describe "PUT #update" do + context "when password is blank" do + it "does not change the existing password" do + original_encrypted = user.encrypted_password + + put :update, params: { id: user.id, user: { password: "", password_confirmation: "" } } + + expect(user.reload.encrypted_password).to eq(original_encrypted) + end + end + + context "when password is provided" do + it "updates the password" do + original_encrypted = user.encrypted_password + + put :update, params: { id: user.id, user: { password: "newpassword123", password_confirmation: "newpassword123" } } + + expect(user.reload.encrypted_password).not_to eq(original_encrypted) + end + end + end +end From 277f87467cc75cded29bc55184e35acfdf4560d3 Mon Sep 17 00:00:00 2001 From: Saverio Trioni <476895+rewritten@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:37:07 +0200 Subject: [PATCH 4/4] fix(admin): use skip_confirmation! to forcibly mark users as confirmed Replace Devise's `confirm` (which requires a pending token and triggers email callbacks) with `skip_confirmation!` + `save`, so the admin action directly sets confirmed_at without any email side-effects. Also updates specs to reflect the correct behaviour. Co-Authored-By: Claude Sonnet 4.6 --- app/admin/user.rb | 3 ++- spec/admin/users_controller_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/admin/user.rb b/app/admin/user.rb index 0ed7ce88..0322c8ee 100644 --- a/app/admin/user.rb +++ b/app/admin/user.rb @@ -25,7 +25,8 @@ end member_action :confirm, method: :put do - if resource.confirm + resource.skip_confirmation! + if resource.save redirect_to admin_user_path(resource), notice: I18n.t("active_admin.users.confirmed_notice") else redirect_to admin_user_path(resource), alert: resource.errors.full_messages.to_sentence diff --git a/spec/admin/users_controller_spec.rb b/spec/admin/users_controller_spec.rb index f8497150..0dfad21c 100644 --- a/spec/admin/users_controller_spec.rb +++ b/spec/admin/users_controller_spec.rb @@ -22,11 +22,11 @@ end context "when the user is already confirmed" do - it "redirects with an alert" do + it "re-confirms and redirects with notice" do put :confirm, params: { id: user.id } expect(response).to redirect_to(admin_user_path(user)) - expect(flash[:alert]).to be_present + expect(flash[:notice]).to eq(I18n.t("active_admin.users.confirmed_notice")) end end end