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
|