Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ gem "audits1984", "~> 0.1.7"

gem "propshaft", "~> 1.3"

gem "mini-levenshtein", "~> 0.1.2"
gem "damerau-levenshtein", "~> 1.3"

gem "faraday", "~> 2.14"
gem "faraday-retry", "~> 2.2"
Expand Down
4 changes: 2 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ GEM
css_parser (2.0.0)
addressable
csv (3.3.5)
damerau-levenshtein (1.3.3)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
Expand Down Expand Up @@ -351,7 +352,6 @@ GEM
marcel (1.1.0)
mcp (0.13.0)
json-schema (>= 4.1)
mini-levenshtein (0.1.2)
mini_magick (5.3.1)
logger
mini_mime (1.1.5)
Expand Down Expand Up @@ -692,6 +692,7 @@ DEPENDENCIES
cloudflare-rails
console1984 (~> 0.2.3)
countries (~> 7.1)
damerau-levenshtein (~> 1.3)
debug
doorkeeper (~> 5.8)
doorkeeper-openid_connect (~> 1.8)
Expand All @@ -717,7 +718,6 @@ DEPENDENCIES
literal (~> 1.9)
lockbox (~> 2.1)
lz_string (~> 0.3.0)
mini-levenshtein (~> 0.1.2)
openssl (~> 3.3)
paper_trail (~> 16.0)
pg (~> 1.6)
Expand Down
58 changes: 46 additions & 12 deletions app/frontend/stylesheets/snippets/banners.scss
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
@mixin banner-base {
display: flex;
align-items: center;
border-radius: 10px;
padding: .875rem 1.125rem;
gap: 0.75rem;
border-radius: $radius-lg;
padding: $space-3 $space-4;
margin-bottom: 1rem;
font-size: 0.9rem;
font-weight: 500;
box-shadow:
0 2px 6px rgba(0, 0, 0, 0.08),
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -1px 1px rgba(0, 0, 0, 0.08);
line-height: 1.5;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);

@include dark-mode {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

& > svg {
height: 1.25rem;
width: 1.25rem;
margin-right: 0.5rem;
flex-shrink: 0;
}
}

@mixin banner-theme($bg-color, $border-color, $fg-color, $list-fg-color) {
background: linear-gradient(145deg,
color-mix(in srgb, $bg-color 100%, white 5%),
background: linear-gradient(145deg,
color-mix(in srgb, $bg-color 100%, white 5%),
$bg-color);
border: 1px solid $border-color;
border: 1.5px solid $border-color;
color: $fg-color;

& > ul, p {
& > ul, & > p {
color: $list-fg-color;
& > li {
color: $list-fg-color;
}
}

h4 {
font-size: 0.9rem;
font-weight: 600;
margin: 0 0 $space-1;
color: $fg-color;
}

ul {
margin: 0;
padding-left: 1.25rem;

li + li { margin-top: 0.25rem; }
}
}

@mixin banner-warning {
Expand Down Expand Up @@ -66,6 +84,16 @@
@include banner-base;
text-align: left;

&:has(h3, h4, p, ul, ol) {
flex-direction: column;
align-items: flex-start;
}

&:has(.banner-action) {
flex-direction: row;
align-items: center;
}

&.warning {
@include banner-warning;
}
Expand All @@ -87,6 +115,12 @@
}
}

.banner-action {
margin-left: auto;
flex-shrink: 0;
white-space: nowrap;
}

.banner-actions {
display: flex;
gap: 0.75rem;
Expand All @@ -105,4 +139,4 @@
align-items: center;
gap: 1rem;
flex-wrap: wrap;
}
}
8 changes: 8 additions & 0 deletions app/models/identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class Identity < ApplicationRecord

has_country_enum

attr_accessor :suggested_email

has_many :sessions, class_name: "IdentitySession", dependent: :destroy
has_many :login_attempts, dependent: :destroy
has_many :login_codes, class_name: "Identity::LoginCode", dependent: :destroy
Expand Down Expand Up @@ -474,6 +476,12 @@ def validate_primary_email
return
end

self.suggested_email = EmailDomainSuggester.suggest(primary_email)
if suggested_email
errors.add(:primary_email, :typo)
return
end

return unless Rails.env.production?

if address.disposable?
Expand Down
53 changes: 53 additions & 0 deletions app/services/email_domain_suggester.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
class EmailDomainSuggester
COMMON_DOMAINS = %w[
gmail.com
icloud.com
outlook.com
outlook.de
outlook.fr
proton.me
hotmail.com
yahoo.com
duck.com
protonmail.com
qq.com
gmx.de
gmx.at
web.de
pm.me
live.com
mail.com
mail.ru
163.com
tutamail.com
mozmail.com
seznam.cz
].freeze

MAX_DISTANCE = 2

def self.suggest(email)
return nil if email.nil? || email.empty? || !email.include?("@")

local, domain = email.split("@", 2)
return nil if domain.nil? || domain.empty?

domain = domain.downcase
return nil if COMMON_DOMAINS.include?(domain)

best_match = nil
best_distance = MAX_DISTANCE + 1

COMMON_DOMAINS.each do |known|
distance = DamerauLevenshtein.distance(domain, known)
if distance <= MAX_DISTANCE && distance < best_distance
best_distance = distance
best_match = known
end
end

return nil unless best_match

"#{local}@#{best_match}"
end
end
2 changes: 1 addition & 1 deletion app/services/papers_please_engine/aadhaar_scrutinizer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def run
aadhaar_name = "#{aadhaar_first_name} #{aadhaar_last_name}".downcase

if identity_name != aadhaar_name
issues << if MiniLevenshtein.edit_distance(identity_name, aadhaar_name) > 4
issues << if DamerauLevenshtein.distance(identity_name, aadhaar_name) > 4
"Name doesn't seem to match"
else
"Name doesn't match exactly (this is probably fine)"
Expand Down
29 changes: 25 additions & 4 deletions app/views/identities/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,33 @@
<% if params[:return_to].present? %>
<%= hidden_field_tag :return_to, params[:return_to] %>
<% end %>
<% if @identity.errors.any? %>
<% if @identity.suggested_email.present? %>
<div class="banner warning" aria-live="polite">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
<span><%= t(".email_typo", suggested: @identity.suggested_email).html_safe %></span>
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
<button type="button" class="secondary small-btn banner-action" onclick="
var suggested = '<%= j @identity.suggested_email %>';
var hidden = document.querySelector('input[type=hidden][name*=primary_email]');
var visible = document.querySelector('input[type=email][name*=primary_email]');
var disabled = document.querySelector('input[type=text][disabled][autocomplete=email]');
if (hidden) hidden.value = suggested;
if (visible) visible.value = suggested;
if (disabled) disabled.value = suggested;
this.closest('form').submit();
">
<%= t(".email_typo_fix") %>
</button>
</div>
<% end %>
<% other_errors = @identity.errors.reject { |e| e.attribute == :primary_email && @identity.suggested_email.present? } %>
<% if other_errors.any? %>
<div class="banner danger" aria-live="polite">
<h4 style="color: var(--error-fg);"><%= pluralize(@identity.errors.count, "issue") %> prevented this from being saved:</h4>
<h4><%= pluralize(other_errors.count, "issue") %> prevented this from being saved:</h4>
<ul>
<% @identity.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% other_errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,8 @@ en:
email_code: We'll send a one-time code to this address.
continue: Continue
wrong_email: "← Wrong email? Go back"
email_typo: "Looks like a typo... did you mean <strong>%{suggested}</strong>?"
email_typo_fix: "yeah, that one! →"
update:
success: saved changes!
create:
Expand Down
Loading