Skip to content

Commit 6428a38

Browse files
kcdragonclaude
andauthored
Let users resend the confirmation email (#35)
New users who lose or expire their confirmation email had no recovery path: they cannot sign in until confirmed and cannot re-register because emails are globally unique. Add a dedicated unauthenticated page (and a link from the sign-in page) where they enter their email to re-send the confirmation link, mirroring the existing password-reset flow. Only unconfirmed accounts are sent mail, and the action always shows the same generic notice so it never reveals whether an email exists or is already confirmed. Reuses the existing RegistrationMailer.confirmation and the on-demand email_confirmation token. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fb6b9da commit 6428a38

5 files changed

Lines changed: 70 additions & 2 deletions

File tree

app/controllers/email_confirmations_controller.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
class EmailConfirmationsController < ApplicationController
2-
allow_unauthenticated_access only: :show
2+
allow_unauthenticated_access only: %i[ new create show ]
3+
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_email_confirmation_path, alert: "Try again later." }
4+
5+
def new
6+
end
7+
8+
def create
9+
user = User.find_by(email_address: params[:email_address])
10+
11+
if user && !user.confirmed?
12+
RegistrationMailer.confirmation(user, Current.organization).deliver_later
13+
end
14+
15+
redirect_to new_session_path, notice: "Confirmation instructions sent (if an unconfirmed account with that email address exists)."
16+
end
317

418
def show
519
if user = User.find_by_token_for(:email_confirmation, params[:token])
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<div class="mx-auto max-w-md w-full px-4 py-16">
2+
<div class="rounded-2xl border border-line bg-surface p-8 shadow-sm">
3+
<h1 class="font-serif font-medium text-3xl tracking-tight text-ink">Resend confirmation email</h1>
4+
5+
<%= form_with url: email_confirmation_path, class: "contents" do |form| %>
6+
<div class="my-5">
7+
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-line bg-surface px-3 py-2 mt-2 w-full focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/10" %>
8+
</div>
9+
10+
<div class="inline">
11+
<%= form.submit "Email confirmation instructions", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-accent hover:bg-[#444] text-white inline-block font-medium cursor-pointer transition" %>
12+
</div>
13+
<% end %>
14+
</div>
15+
</div>

app/views/sessions/new.html.erb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
<%= link_to "Sign up", new_registration_path, class: "text-ink hover:text-brand underline" %>
2121
&middot;
2222
<%= link_to "Forgot password?", new_password_path, class: "text-ink hover:text-brand underline" %>
23+
&middot;
24+
<%= link_to "Resend confirmation", new_email_confirmation_path, class: "text-ink hover:text-brand underline" %>
2325
</div>
2426
</div>
2527
<% end %>

config/routes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
resource :home
44
resources :passwords, param: :token
55
resource :registration, only: %i[ new create ]
6-
resource :email_confirmation, only: :show
6+
resource :email_confirmation, only: %i[ new create show ]
77
resource :session
88

99
resources :scenarios do

test/controllers/email_confirmations_controller_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,38 @@ class EmailConfirmationsControllerTest < ActionDispatch::IntegrationTest
66
@user = users(:unconfirmed)
77
end
88

9+
test "new" do
10+
get new_email_confirmation_path
11+
assert_response :success
12+
end
13+
14+
test "create for an unconfirmed user enqueues a confirmation email" do
15+
post email_confirmation_path, params: { email_address: @user.email_address }
16+
assert_enqueued_email_with RegistrationMailer, :confirmation, args: [ @user, organizations(:arlington) ]
17+
assert_redirected_to new_session_path
18+
19+
follow_redirect!
20+
assert_notice "Confirmation instructions sent"
21+
end
22+
23+
test "create for an already-confirmed user sends no mail" do
24+
post email_confirmation_path, params: { email_address: users(:one).email_address }
25+
assert_enqueued_emails 0
26+
assert_redirected_to new_session_path
27+
28+
follow_redirect!
29+
assert_notice "Confirmation instructions sent"
30+
end
31+
32+
test "create for an unknown email sends no mail and reveals nothing" do
33+
post email_confirmation_path, params: { email_address: "missing-user@example.com" }
34+
assert_enqueued_emails 0
35+
assert_redirected_to new_session_path
36+
37+
follow_redirect!
38+
assert_notice "Confirmation instructions sent"
39+
end
40+
941
test "show with a valid token confirms the user and signs them in" do
1042
token = @user.generate_token_for(:email_confirmation)
1143

@@ -33,4 +65,9 @@ class EmailConfirmationsControllerTest < ActionDispatch::IntegrationTest
3365
assert_redirected_to new_session_path
3466
assert_nil cookies[:session_id]
3567
end
68+
69+
private
70+
def assert_notice(text)
71+
assert_select "div", /#{text}/
72+
end
3673
end

0 commit comments

Comments
 (0)