diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 15523c3..1ba9419 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -19,6 +19,7 @@ class UsersController < ApplicationController def update authorize @user + @user.password_change_attempt = false if @user.update(user_params) redirect_to contests_path, notice: t("users.edit.notice") else @@ -26,6 +27,18 @@ class UsersController < ApplicationController end end + def change_password + @user = User.find(params[:user_id]) + authorize @user + + @user.password_change_attempt = true + if @user.update(user_password_params) + redirect_to contests_path, notice: t("users.edit.notice") + else + render :edit, status: :unprocessable_entity + end + end + def show authorize @user @@ -89,6 +102,10 @@ class UsersController < ApplicationController end def user_params - params.expect(user: [ :username, :email_address, :lang, :password ]) + params.expect(user: [ :username, :email_address, :lang ]) + end + + def user_password_params + params.expect(user: [ :password ]) end end diff --git a/app/models/user.rb b/app/models/user.rb index 3a2502a..7128e69 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,14 +2,15 @@ # # Table name: users # -# id :integer not null, primary key -# admin :boolean default(FALSE), not null -# email_address :string not null -# lang :string default("en") -# password_digest :string not null -# username :string -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# admin :boolean default(FALSE), not null +# email_address :string not null +# lang :string default("en") +# password_change_attempt :boolean +# password_digest :string not null +# username :string +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # @@ -25,4 +26,5 @@ class User < ApplicationRecord validates :username, presence: true, uniqueness: true validates :email_address, presence: true, uniqueness: true validates :lang, inclusion: { in: Languages::AVAILABLE_LANGUAGES.map { |lang| lang[:id] } } + validates :password, presence: true, if: -> { password_change_attempt } end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index b30538f..4051ebc 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -20,7 +20,11 @@ class UserPolicy < ApplicationPolicy end def update? - user.admin? || user.id == record.id + edit? + end + + def change_password? + edit? end def destroy? diff --git a/app/views/users/_form.html.slim b/app/views/users/_form.html.slim index 9fdb945..a750207 100644 --- a/app/views/users/_form.html.slim +++ b/app/views/users/_form.html.slim @@ -30,13 +30,13 @@ = form.label :password, class: "required" = form.submit t("helpers.buttons.save"), class: "btn btn-primary" - - if method == :patch - h4.mt-5 = t("users.edit.password_section") +- if method == :patch + h4.mt-5 = t("users.edit.password_section") - = form_with model: user, method: method do |form| - .row.mb-3 - .col - .form-floating - = form.password_field :password, autocomplete: "off", class: "form-control" - = form.label :password, class: "required" - = form.submit t("helpers.buttons.save"), class: "btn btn-primary" \ No newline at end of file + = form_with model: user, url: user_password_path(user) do |form| + .row.mb-3 + .col + .form-floating + = form.password_field :password, autocomplete: "off", class: "form-control" + = form.label :password, class: "required" + = form.submit t("helpers.buttons.save_password"), class: "btn btn-primary" \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index a77e229..6a8095e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -167,6 +167,9 @@ en: blank: Your email cannot be empty username: blank: Your username cannot be empty + taken: This username is already taken + password: + blank: Your password cannot be empty categories: destroy: notice: Category deleted @@ -290,6 +293,7 @@ en: settings: Settings sign_in: Sign in save: Save + save_password: Save password start: Click here to start your participation stopwatch_continue: Continue stopwatch_pause: Pause @@ -352,7 +356,7 @@ en: notice: Settings updated title: "My settings" general_section: "General settings" - password_section: "Change password" + password_section: "Password" index: title: "All users" new: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 513c335..e65e913 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -138,6 +138,9 @@ fr: blank: L'email est obligatoire username: blank: Le nom d'utilisateur.ice est obligatoire + taken: Ce nom d'utilisateur.ice est déjà utilisé + password: + blank: Le mot de passe ne peut pas être vide categories: destroy: notice: Catégorie supprimée @@ -261,6 +264,7 @@ fr: settings: Paramètres sign_in: Se connecter save: Modifier + save_password: Modifier le mot de passe start: Clique ici pour démarrer ta participation stopwatch_continue: Reprendre stopwatch_pause: Pause @@ -323,7 +327,7 @@ fr: notice: Paramètres modifiés title: "Mes paramètres" general_section: "Paramètres globaux" - password_section: "Modifier mon mot de passe" + password_section: "Mot de passe" index: title: "Tous.tes les utilisateur.ices" new: diff --git a/config/routes.rb b/config/routes.rb index c4e5267..8b1f712 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,7 +40,9 @@ Rails.application.routes.draw do end resources :passwords, param: :token resource :session - resources :users + resources :users do + patch "password", to: "users#change_password" + end options "connect", to: "messages#cors_preflight_check" options "message", to: "messages#cors_preflight_check" diff --git a/db/migrate/20251210092658_add_password_change_attempt_to_user.rb b/db/migrate/20251210092658_add_password_change_attempt_to_user.rb new file mode 100644 index 0000000..6d7bb10 --- /dev/null +++ b/db/migrate/20251210092658_add_password_change_attempt_to_user.rb @@ -0,0 +1,5 @@ +class AddPasswordChangeAttemptToUser < ActiveRecord::Migration[8.0] + def change + add_column :users, :password_change_attempt, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 2e8690b..4548a40 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_09_081941) do +ActiveRecord::Schema[8.0].define(version: 2025_12_10_092658) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -185,6 +185,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_09_081941) do t.string "username" t.boolean "admin", default: false, null: false t.string "lang", default: "en" + t.boolean "password_change_attempt" t.index ["email_address"], name: "index_users_on_email_address", unique: true end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 7f0cd8c..1ee82be 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -2,14 +2,15 @@ # # Table name: users # -# id :integer not null, primary key -# admin :boolean default(FALSE), not null -# email_address :string not null -# lang :string default("en") -# password_digest :string not null -# username :string -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# admin :boolean default(FALSE), not null +# email_address :string not null +# lang :string default("en") +# password_change_attempt :boolean +# password_digest :string not null +# username :string +# created_at :datetime not null +# updated_at :datetime not null # # Indexes # diff --git a/spec/features/user_spec.rb b/spec/features/user_spec.rb index ef12ba6..9873135 100644 --- a/spec/features/user_spec.rb +++ b/spec/features/user_spec.rb @@ -21,21 +21,64 @@ RSpec.feature "Users", type: :feature do expect(page).not_to have_content(I18n.t("users.index.title")) end - it "should be able to create a new contest" do + it "should be able to open their account info" do visit root_path - click_link "Create a new contest" + click_link I18n.t("nav.settings") - expect(page).to have_content(I18n.t("contests.new.title")) + expect(page).to have_current_path(edit_user_path(user)) end - it "should be able to open an existing contest" do - visit root_path + context "when updating their account info" do + let!(:existing_user) { create(:user, username: "taken_username") } - expect(page).to have_content(contest.name) - find("div.card", text: contest.name).find("a").click + it "should allow changing to an untaken username" do + visit edit_user_path(user) - expect(page).to have_content(I18n.t("contests.show.title", name: contest.name)) + fill_in I18n.t("activerecord.attributes.user.username"), with: "untaken_username" + + expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.to change(user, :username).to("untaken_username") + end + + it "should prevent changing to an already taken username" do + visit edit_user_path(user) + + fill_in I18n.t("activerecord.attributes.user.username"), with: "taken_username" + + expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.not_to change(user, :username) + + expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.username.taken")) + end + + it "should prevent changing to a blank username" do + visit edit_user_path(user) + + fill_in I18n.t("activerecord.attributes.user.username"), with: "" + + expect { click_button(I18n.t("helpers.buttons.save")); user.reload }.not_to change(user, :username) + + expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.username.blank")) + end + + it "should allow changing to a non-blank password" do + visit edit_user_path(user) + + fill_in I18n.t("activerecord.attributes.user.password"), with: "new_password" + + expect { click_button(I18n.t("helpers.buttons.save_password")); user.reload } + .to change(user, :password_digest) + .and change { user.authenticate("new_password") }.from(false).to(user) + end + + it "should prevent changing to a blank password" do + visit edit_user_path(user) + + fill_in I18n.t("activerecord.attributes.user.password"), with: "" + + expect { click_button(I18n.t("helpers.buttons.save_password")); user.reload }.not_to change(user, :password) + + expect(page).to have_content(I18n.t("activerecord.errors.models.user.attributes.password.blank")) + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6da689a..2936a39 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,14 +2,15 @@ # # Table name: users # -# id :integer not null, primary key -# admin :boolean default(FALSE), not null -# email_address :string not null -# lang :string default("en") -# password_digest :string not null -# username :string -# created_at :datetime not null -# updated_at :datetime not null +# id :integer not null, primary key +# admin :boolean default(FALSE), not null +# email_address :string not null +# lang :string default("en") +# password_change_attempt :boolean +# password_digest :string not null +# username :string +# created_at :datetime not null +# updated_at :datetime not null # # Indexes #