Skip to content

Conversation

@lodewiges
Copy link
Contributor

@lodewiges lodewiges commented Jan 7, 2026

This PR adds the option to login with a emailadress so when people forget there password and username the can still login in

Summary by CodeRabbit

  • New Features

    • Users can log in and recover forgotten passwords using either their username or email address.
  • Improvements

    • Login and password-recovery labels now show "username or email".
    • Login input is trimmed, case-insensitive, and nil-safe; username matches preferred when both apply.
  • Tests

    • Expanded unit tests covering username/email matching, case/whitespace handling, and edge cases.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 7, 2026 15:06
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Replaces direct username lookups with normalized login helpers in SofiaAccount, updates Devise locate_conditions to use the resolver, adjusts login/forgot-password labels to indicate "username or e‑mail", and adds unit tests for the new lookup behavior.

Changes

Cohort / File(s) Change Summary
Model: login lookup helpers
app/models/sofia_account.rb
Added self.find_for_login(identifier) (nil-safe, trims input, joins users, matches sofia_accounts.username or users.email) and self.resolve_login_identifier(identifier) (returns username via resolver).
Controller: forgot-password flow
app/controllers/sofia_accounts_controller.rb
Replaced find_by(username: ...) with SofiaAccount.find_for_login(...) in forgot-password handling; downstream token/email behavior unchanged.
Views: input labels
app/views/sofia_accounts/login.html.erb, app/views/sofia_accounts/forgot_password_view.html.erb
Labels updated from "Gebruikersnaam" to "Gebruikersnaam of e-mailadres"; input names/attributes unchanged.
Auth config: Devise/OmniAuth
config/initializers/devise.rb
Devise locate_conditions now normalizes incoming auth_key via SofiaAccount.resolve_login_identifier(...) before matching SofiaAccount.auth_key.
Tests
spec/models/sofia_account_spec.rb
Extensive unit tests added for .find_for_login and .resolve_login_identifier covering username/email matching, case/whitespace handling, nil/empty inputs, and multiple-account scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Browser
  participant Controller as SofiaAccountsController
  participant Model as SofiaAccount
  participant Devise

  Note over User,Browser: User submits auth_key (username or email)
  User->>Browser: submit auth_key
  Browser->>Controller: POST /login or /forgot_password (auth_key)
  Controller->>Model: find_for_login(auth_key)
  alt match found
    Model-->>Controller: SofiaAccount (with User)
    Controller->>Devise: proceed (authenticate or trigger reset)
    Devise-->>Browser: success / redirect
  else no match
    Controller-->>Browser: not-found / error response
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

status:ready to review

Poem

🐰 I hop through code with ears alert,
Username or mail — I'll trim the dirt.
Find the match, then send the key,
A tiny hop for account recovery. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete against the repository template. It lacks the required checklist section, database migration testing confirmation, summary of changes, and other relevant information sections. Fill out all required template sections: complete the migration checklist, provide a detailed summary of UI changes, reference related issues with 'fixes #xyz', and document any relevant dependencies or conventions.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title refers to the main change in the changeset—adding email login functionality—but contains a minor typo ('emailadres' instead of 'email address') and uses 'add to option' instead of 'add option', making it slightly awkward phrasing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the ability for users to log in using their email address in addition to their username. The changes update both the authentication logic and user-facing labels to support this new functionality.

  • Implements email-based login lookup in the SofiaAccount model
  • Updates Devise initializer to resolve login identifiers (email or username) to usernames
  • Updates UI labels in login and password reset views to indicate users can enter either username or email

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
app/models/sofia_account.rb Adds find_by_login and resolve_login_identifier methods to support finding accounts by username or email
config/initializers/devise.rb Updates OmniAuth identity configuration to use the new identifier resolution method
app/controllers/sofia_accounts_controller.rb Updates forgot password action to use the new find_by_login method
app/views/sofia_accounts/login.html.erb Updates label from "Gebruikersnaam" to "Gebruikersnaam of e-mailadres"
app/views/sofia_accounts/forgot_password_view.html.erb Updates label from "Gebruikersnaam" to "Gebruikersnaam of e-mailadres"

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
app/models/sofia_account.rb (1)

36-41: Consider case-insensitive email matching for better UX.

The email lookup at line 40 is case-sensitive, which could lead to authentication failures when users enter their email with different casing (e.g., User@Example.com vs user@example.com).

♻️ Proposed fix for case-insensitive email matching
 def self.find_by_login(identifier)
   return nil if identifier.blank?

   trimmed = identifier.to_s.strip
-  find_by(username: trimmed) || joins(:user).find_by(users: { email: trimmed })
+  find_by(username: trimmed) || joins(:user).find_by('LOWER(users.email) = LOWER(?)', trimmed)
 end

Note: This assumes PostgreSQL or a database that supports the LOWER function. For MySQL, the syntax is similar. Alternatively, if the users.email column already has a case-insensitive collation, the current code is fine.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 656f5a2 and 284e308.

📒 Files selected for processing (5)
  • app/controllers/sofia_accounts_controller.rb
  • app/models/sofia_account.rb
  • app/views/sofia_accounts/forgot_password_view.html.erb
  • app/views/sofia_accounts/login.html.erb
  • config/initializers/devise.rb
🧰 Additional context used
🧬 Code graph analysis (2)
app/controllers/sofia_accounts_controller.rb (2)
app/models/sofia_account.rb (1)
  • find_by_login (36-41)
webpack.config.js (1)
  • require (5-5)
config/initializers/devise.rb (1)
app/models/sofia_account.rb (1)
  • resolve_login_identifier (43-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: Build
🔇 Additional comments (5)
app/views/sofia_accounts/forgot_password_view.html.erb (1)

10-10: LGTM!

The label update accurately reflects the new dual-identifier login capability.

app/models/sofia_account.rb (1)

43-45: LGTM!

The method cleanly wraps find_by_login to provide the username for Devise authentication lookup.

app/views/sofia_accounts/login.html.erb (1)

24-24: LGTM!

The label update is consistent with the forgot password view and accurately describes the new functionality.

app/controllers/sofia_accounts_controller.rb (1)

129-129: LGTM!

The updated method call correctly uses find_by_login to enable email-based password reset. The existing nil handling and generic error message appropriately protect against account enumeration.

config/initializers/devise.rb (1)

21-24: The implementation already handles nil identifiers correctly. When resolve_login_identifier returns nil (identifier not found), the locate_conditions attempts to match { username: nil }, which fails to find an account. The CallbacksController.identity method properly detects this case and returns a user-facing error message: "Inloggen mislukt. De ingevulde gegevens zijn incorrect." This is confirmed by the test spec which validates the behavior for non-existent accounts. No changes needed.

@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.72%. Comparing base (656f5a2) to head (e8a5470).

Additional details and impacted files
@@             Coverage Diff             @@
##           staging    #1196      +/-   ##
===========================================
+ Coverage    77.62%   77.72%   +0.09%     
===========================================
  Files           54       54              
  Lines         1341     1347       +6     
===========================================
+ Hits          1041     1047       +6     
  Misses         300      300              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

lodewiges and others added 2 commits January 7, 2026 16:17
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @app/models/sofia_account.rb:
- Around line 45-47: Add unit tests for SofiaAccount.find_by_login and
SofiaAccount.resolve_login_identifier covering: successful lookup by username
and by email, case-insensitivity for both username and email, trimming
surrounding whitespace, handling nil/blank input (should return nil or
appropriate falsey), and behavior when no account matches (returns nil). Write
specs that exercise both methods directly (call find_by_login and
resolve_login_identifier) and assert expected return values, using unique
example usernames/emails and variations in case/whitespace to verify
normalization behavior.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 284e308 and 0855f8a.

📒 Files selected for processing (1)
  • app/models/sofia_account.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build

Comment on lines 45 to 47
def self.resolve_login_identifier(identifier)
find_by_login(identifier)&.username
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the test file for SofiaAccount
find . -type f -name "*sofia_account*" | grep -i spec

Repository: csvalpha/sofia

Length of output: 259


🏁 Script executed:

# Search for tests of find_by_login and resolve_login_identifier
rg -nP --type=ruby 'find_by_login|resolve_login_identifier' --glob='**/spec/**'

Repository: csvalpha/sofia

Length of output: 40


🏁 Script executed:

# Also check the actual implementation to understand both methods better
cat -n app/models/sofia_account.rb | head -60

Repository: csvalpha/sofia

Length of output: 2327


🏁 Script executed:

cat -n spec/models/sofia_account_spec.rb

Repository: csvalpha/sofia

Length of output: 4196


Add tests for authentication methods find_by_login and resolve_login_identifier.

The SofiaAccount model lacks test coverage for these two methods. Given the repository's comprehensive test suite for SofiaAccount, these authentication methods require tests covering:

  • Finding by username
  • Finding by email
  • Case sensitivity handling (username and email)
  • Whitespace trimming
  • Nil/blank input handling
  • Behavior when no match is found
🤖 Prompt for AI Agents
In @app/models/sofia_account.rb around lines 45 - 47, Add unit tests for
SofiaAccount.find_by_login and SofiaAccount.resolve_login_identifier covering:
successful lookup by username and by email, case-insensitivity for both username
and email, trimming surrounding whitespace, handling nil/blank input (should
return nil or appropriate falsey), and behavior when no account matches (returns
nil). Write specs that exercise both methods directly (call find_by_login and
resolve_login_identifier) and assert expected return values, using unique
example usernames/emails and variations in case/whitespace to verify
normalization behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @app/controllers/sofia_accounts_controller.rb:
- Line 129: The current SofiaAccount.find_for_login implementation loads all
records with non-nil emails and searches in Ruby; replace it with a
database-level, case-insensitive lookup in the SofiaAccount model to avoid
loading all users into memory. Update the find_for_login(login) method to
normalize the login (downcase for email comparisons) and perform a single query
that matches either a case-insensitive email (use LOWER(email) = ? or ILIKE for
Postgres) or an exact username match, returning the first matching record (e.g.,
use where(...).first or find_by with a sanitized SQL condition) so the filtering
is done by the DB instead of Ruby iteration.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed32c63 and 4a8eb94.

📒 Files selected for processing (2)
  • app/controllers/sofia_accounts_controller.rb
  • app/models/sofia_account.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/models/sofia_account.rb
🧰 Additional context used
🧬 Code graph analysis (1)
app/controllers/sofia_accounts_controller.rb (1)
app/models/sofia_account.rb (1)
  • find_for_login (36-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build

def forgot_password # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
generic_message = 'Als dit account bestaat, is er een email verstuurd.'
sofia_account = SofiaAccount.find_by(username: params.require(:username))
sofia_account = SofiaAccount.find_for_login(params.require(:username))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the sofia_account.rb file
fd "sofia_account" -t f

Repository: csvalpha/sofia

Length of output: 518


🏁 Script executed:

#!/bin/bash
# Locate and examine the find_for_login method
rg -n "find_for_login" -A 10 -B 2

Repository: csvalpha/sofia

Length of output: 2044


🏁 Script executed:

#!/bin/bash
# Check the User model for relationships
fd "user\.rb" -t f | head -5

Repository: csvalpha/sofia

Length of output: 158


🏁 Script executed:

#!/bin/bash
# Check the SofiaAccount model structure and relationships
fd "sofia_account\.rb" -t f -x cat {}

Repository: csvalpha/sofia

Length of output: 2292


🏁 Script executed:

#!/bin/bash
# Check the User model for the relationship and email handling
head -100 app/models/user.rb

Repository: csvalpha/sofia

Length of output: 3254


🏁 Script executed:

#!/bin/bash
# Check if there are any indexes on the email column in users table
rg -n "email" db/migrate/*.rb | grep -i "index\|add_column.*email"

Repository: csvalpha/sofia

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check how find_for_login is used in the codebase
rg -n "find_for_login" -B 2 -A 2

Repository: csvalpha/sofia

Length of output: 1061


🏁 Script executed:

#!/bin/bash
# Check for email indexes in all migrations
rg "add_index.*:users.*:email|add_index.*users.*email" -A 2 -B 2

Repository: csvalpha/sofia

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check the database schema to see current indexes
fd "schema.rb" -t f -x cat {} | grep -A 30 "t.string.*email"

Repository: csvalpha/sofia

Length of output: 2416


🏁 Script executed:

#!/bin/bash
# Verify the exact relationship - check if join would work as suggested
rg "belongs_to.*user|has_one.*sofia" app/models/sofia_account.rb app/models/user.rb

Repository: csvalpha/sofia

Length of output: 169


Critical performance issue in the find_for_login implementation.

The change correctly enables email-based lookup for password reset, but the find_for_login method in app/models/sofia_account.rb (lines 36-41) loads all users with non-nil emails into memory and iterates through them using Ruby's find method. This will cause severe performance degradation as your user base grows.

🐛 Recommended fix: use database-level case-insensitive query

In app/models/sofia_account.rb, replace the current implementation with a proper database query:

 def self.find_for_login(identifier)
   return nil if identifier.blank?

   trimmed = identifier.to_s.strip
-  find_by(username: trimmed) || User.includes(:sofia_account).where.not(email: nil).find { |u| u.email.casecmp?(trimmed) }&.sofia_account
+  find_by(username: trimmed) || 
+    joins(:user).where('LOWER(users.email) = LOWER(?)', trimmed).first
 end

This performs the case-insensitive email match at the database level instead of loading all users into memory.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @app/controllers/sofia_accounts_controller.rb at line 129, The current
SofiaAccount.find_for_login implementation loads all records with non-nil emails
and searches in Ruby; replace it with a database-level, case-insensitive lookup
in the SofiaAccount model to avoid loading all users into memory. Update the
find_for_login(login) method to normalize the login (downcase for email
comparisons) and perform a single query that matches either a case-insensitive
email (use LOWER(email) = ? or ILIKE for Postgres) or an exact username match,
returning the first matching record (e.g., use where(...).first or find_by with
a sanitized SQL condition) so the filtering is done by the DB instead of Ruby
iteration.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @spec/models/sofia_account_spec.rb:
- Around line 110-118: The tests assume case-insensitive username lookup but
find_for_login currently uses find_by(username: trimmed) which is DB-collation
dependent; update described_class.find_for_login to perform an explicit
case-insensitive match (e.g., use a SQL lower comparison like 'LOWER(username) =
?' with trimmed.downcase) instead of relying on find_by, while keeping the
existing email comparison using casecmp?; alternatively, if you prefer DB-side
fixes, ensure the username column has a case-insensitive collation, but the
immediate code change should be to normalize/compare username lowercased in
find_for_login so tests behave consistently across databases.
🧹 Nitpick comments (1)
spec/models/sofia_account_spec.rb (1)

120-133: Consider parameterizing repetitive whitespace tests.

The whitespace handling tests (leading, trailing, surrounding) are thorough but repetitive. Consider using RSpec's parameterized tests or shared examples to reduce duplication while maintaining clarity.

Example using RSpec's parameterized approach
context 'when searching by username' do
  [
    ['exact match', 'testuser'],
    ['uppercase', 'TESTUSER'],
    ['mixed case', 'TestUser'],
    ['leading whitespace', '  testuser'],
    ['trailing whitespace', 'testuser  '],
    ['surrounding whitespace', '  testuser  ']
  ].each do |description, input|
    it "finds account by username with #{description}" do
      result = described_class.find_for_login(input)
      expect(result).to eq(account)
    end
  end
end

However, the current explicit approach is perfectly acceptable and may be clearer for some teams.

Also applies to: 156-169, 269-277

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a8eb94 and e8a5470.

📒 Files selected for processing (1)
  • spec/models/sofia_account_spec.rb
🧰 Additional context used
🧬 Code graph analysis (1)
spec/models/sofia_account_spec.rb (1)
app/models/sofia_account.rb (2)
  • find_for_login (36-41)
  • resolve_login_identifier (43-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build
🔇 Additional comments (3)
spec/models/sofia_account_spec.rb (3)

136-176: Excellent test coverage for email-based login.

The email lookup tests comprehensively cover case-insensitive matching, whitespace handling, and username precedence. The explicit use of casecmp? in the implementation guarantees consistent case-insensitive behavior across environments.


178-212: Thorough edge case coverage.

The nil/blank input handling and non-existent account scenarios are well-tested, including the important case where users with nil emails are correctly excluded from email lookups.


240-337: Well-structured tests for identifier resolution.

The resolve_login_identifier tests effectively verify that both username and email inputs correctly resolve to the account's username. The coverage of normalization (case, whitespace) and non-resolution scenarios is comprehensive.

Note: The same username case-sensitivity concern flagged for find_for_login applies here, as this method delegates to it.

Comment on lines +110 to +118
it 'finds account by username regardless of case' do
result = described_class.find_for_login('TESTUSER')
expect(result).to eq(account)
end

it 'finds account by username with mixed case' do
result = described_class.find_for_login('TestUser')
expect(result).to eq(account)
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check database schema for sofia_accounts table and username column
fd schema.rb -x cat {} | rg -A 20 'create_table.*sofia_accounts'

Repository: csvalpha/sofia

Length of output: 880


🏁 Script executed:

# Search for migrations related to sofia_accounts or username
fd -e rb . db/migrate/ | head -20

Repository: csvalpha/sofia

Length of output: 1351


🏁 Script executed:

# Check for any collation or case-insensitive settings
rg -i 'collat|citext' db/

Repository: csvalpha/sofia

Length of output: 40


🏁 Script executed:

# Locate the find_for_login implementation
rg -A 15 'def.*find_for_login' --type ruby

Repository: csvalpha/sofia

Length of output: 722


Verify database collation supports case-insensitive username matching or add explicit case normalization.

These tests assume case-insensitive username lookups (lines 110-118, 232-236: 'TESTUSER' finding 'testuser'), but the implementation uses find_by(username: trimmed), which is case-sensitive by default unless the database specifies a case-insensitive collation. The schema shows no explicit collation settings on the username column, creating environment-dependent behavior where tests may pass in development (SQLite) but fail in production (PostgreSQL/MySQL without case-insensitive collation).

Email lookups explicitly use casecmp? for guaranteed case-insensitivity, but usernames rely on database configuration. For consistency and reliability, either:

  1. Configure the database column with case-insensitive collation, or
  2. Update the implementation to use explicit case normalization:
find_by('LOWER(username) = ?', trimmed.downcase) || User.includes(:sofia_account).where.not(email: nil).find { |u| u.email.casecmp?(trimmed) }&.sofia_account
🤖 Prompt for AI Agents
In @spec/models/sofia_account_spec.rb around lines 110 - 118, The tests assume
case-insensitive username lookup but find_for_login currently uses
find_by(username: trimmed) which is DB-collation dependent; update
described_class.find_for_login to perform an explicit case-insensitive match
(e.g., use a SQL lower comparison like 'LOWER(username) = ?' with
trimmed.downcase) instead of relying on find_by, while keeping the existing
email comparison using casecmp?; alternatively, if you prefer DB-side fixes,
ensure the username column has a case-insensitive collation, but the immediate
code change should be to normalize/compare username lowercased in find_for_login
so tests behave consistently across databases.

@lodewiges lodewiges marked this pull request as draft January 7, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants