diff --git a/app/frontend/stylesheets/application.tailwind.css b/app/frontend/stylesheets/application.tailwind.css index 2400cbbc1..a1158f0ca 100644 --- a/app/frontend/stylesheets/application.tailwind.css +++ b/app/frontend/stylesheets/application.tailwind.css @@ -161,18 +161,13 @@ color: #374151; /* gray-700 */ } -/* Hide Chrome / Edge / Safari (macOS + iOS) password reveal icon */ -input[type="password"]::-webkit-textfield-decoration-container { - visibility: hidden; - pointer-events: none; - width: 0; - height: 0; +/* Hide Chrome / Edge / Safari built-in password reveal icon (we use our own toggle) */ +input[type="password"]::-ms-reveal { + display: none; } input[type="password"]::-webkit-credentials-auto-fill-button { display: none !important; - visibility: hidden; - pointer-events: none; } /* Safari fix: Ensure password field has proper height */ diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 81fac9c80..f8c3f27a5 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -57,7 +57,7 @@ <% if user.confirmed_at.present? %> - + <% elsif user.welcome_instructions_sent_at.present? %> <% sent_info = "Confirmation instructions sent #{l(user.welcome_instructions_sent_at, format: :long)}" sent_info += " by #{user.updated_by.name}" if user.updated_by.present? %> diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index b8f22ffc8..2e1fe87d4 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -183,7 +183,7 @@ config.unlock_in = nil # Warn on the last attempt before the account is locked. - # config.last_attempt_warning = true + config.last_attempt_warning = true # ==> Configuration for :recoverable # diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 126b3bbcf..da531e7e2 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -8,11 +8,11 @@ en: send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: already_authenticated: "You are already signed in." - inactive: "Your account is currently inactive. Please email %{organization_name} to find out more." - invalid: "Invalid %{authentication_keys} or password." - locked: "Your account is locked." + inactive: "Invalid email or password. Please contact us for assistance." + invalid: "Invalid email or password. Please contact us for assistance." + locked: "Please contact us for assistance." last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." + not_found_in_database: "Invalid email or password. Please contact us for assistance." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." diff --git a/spec/system/login_spec.rb b/spec/system/login_spec.rb index 785f48777..c955b1d94 100644 --- a/spec/system/login_spec.rb +++ b/spec/system/login_spec.rb @@ -1,26 +1,80 @@ -# require "rails_helper" -# -# RSpec.describe "User login", type: :system do -# let(:user) { create(:user) } -# -# it "shows default avatar when logged out" do -# visit root_path -# expect(page).to_not have_css("#avatar") -# expect(page).to_not have_css("img[src*='missing.png']") -# end -# -# scenario "User login shows avatar only after login" do -# visit root_path -# -# # Logged out state -# expect(page).not_to have_css("#avatar") -# -# # Log in -# sign_in user -# visit root_path -# -# # Logged in state -# expect(page).to have_css("#avatar") -# expect(page).to have_css("#avatar-image") -# end -# end +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe "User login", type: :system do + let(:password) { "MyString" } + let(:generic_error) { "Invalid email or password. Please contact us for assistance." } + + def fill_in_login(email, password) + visit new_user_session_path + fill_in "user_email", with: email + fill_in "user_password", with: password + click_button "Log in" + end + + context "when user is locked" do + let(:user) { create(:user, :locked, password: password) } + + it "does not allow login and shows locked message" do + fill_in_login(user.email, password) + + expect(page).to have_current_path(new_user_session_path) + expect(page).to have_content("Please contact us for assistance.") + end + end + + context "when user is inactive" do + let(:user) { create(:user, password: password, inactive: true) } + + it "does not allow login and shows generic error" do + fill_in_login(user.email, password) + + expect(page).to have_current_path(new_user_session_path) + expect(page).to have_content(generic_error) + end + end + + context "when user exceeds maximum failed login attempts" do + let(:user) { create(:user, password: password) } + let(:last_attempt_warning) { "You have one more attempt before your account is locked" } + + it "warns on the last attempt then locks the account" do + 9.times do + fill_in_login(user.email, "wrong_password") + end + + expect(page).to have_content(last_attempt_warning) + + fill_in_login(user.email, "wrong_password") + + fill_in_login(user.email, password) + + expect(page).to have_current_path(new_user_session_path) + expect(page).to have_content("Please contact us for assistance.") + expect(user.reload.locked_at).to be_present + end + end + + context "when credentials are wrong" do + let(:user) { create(:user, password: password) } + + it "shows generic error" do + fill_in_login(user.email, "wrong_password") + + expect(page).to have_current_path(new_user_session_path) + expect(page).to have_content(generic_error) + end + end + + context "when user is active and unlocked" do + let(:user) { create(:user, password: password) } + + it "allows login successfully" do + fill_in_login(user.email, password) + + expect(page).not_to have_content(generic_error) + expect(page).to have_css("#avatar") + end + end +end