feat: ensure signup verification [UPM-14] (#13858)

Previously, signing up gave immediate access to the app. Now,
unconfirmed users are redirected to a verification page where they can
resend the confirmation email.

- After signup, the user is routed to `/auth/verify-email` instead of
the dashboard
- After login, unconfirmed users are redirected to the verification page
- The dashboard route guard catches unconfirmed users and redirects them
- `active_for_authentication?` is removed from the sessions controller
so unconfirmed users can authenticate — the frontend gates access
instead
- If the user visits the verification page after already confirming,
they're automatically redirected to the dashboard
- No session is issued until the user is verified

<details><summary>Demo</summary>
<p>

#### Fresh Signup


https://github.com/user-attachments/assets/abb735e5-7c8e-44a2-801c-96d9e4823e51

#### Google Fresh Signup


https://github.com/user-attachments/assets/ab9e389a-a604-4a9d-b492-219e6d94ee3f


#### Create new account from Dashboard


https://github.com/user-attachments/assets/c456690d-1946-4e0b-834b-ad8efcea8369



</p>
</details>

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Shivam Mishra
2026-04-07 13:45:17 +05:30
committed by GitHub
parent fbe3560b7a
commit 4f94ad4a75
11 changed files with 316 additions and 12 deletions

View File

@@ -120,8 +120,20 @@ class Rack::Attack
end
end
## Resend confirmation throttling
## Resend confirmation throttling (unauthenticated)
throttle('resend_confirmation/ip', limit: 5, period: 30.minutes) do |req|
req.ip if req.path_without_extentions == '/resend_confirmation' && req.post?
end
throttle('resend_confirmation/email', limit: 5, period: 1.hour) do |req|
if req.path_without_extentions == '/resend_confirmation' && req.post?
email = req.params['email'].presence || ActionDispatch::Request.new(req.env).params['email'].presence
email.to_s.downcase.gsub(/\s+/, '')
end
end
## Resend confirmation throttling (authenticated)
throttle('resend_confirmation_auth/ip', limit: 5, period: 30.minutes) do |req|
req.ip if req.path_without_extentions == '/api/v1/profile/resend_confirmation' && req.post?
end

View File

@@ -8,6 +8,8 @@ Rails.application.routes.draw do
omniauth_callbacks: 'devise_overrides/omniauth_callbacks'
}, via: [:get, :post]
post 'resend_confirmation', to: 'auth/resend_confirmations#create'
## renders the frontend paths only if its not an api only server
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false))
root to: 'api#index'