Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/stevepolitodesign/approaches-to-confirming-user-emails-in-rails
Learn how to use Signed IDs to effectively confirm accounts in Rails
https://github.com/stevepolitodesign/approaches-to-confirming-user-emails-in-rails
Last synced: about 17 hours ago
JSON representation
Learn how to use Signed IDs to effectively confirm accounts in Rails
- Host: GitHub
- URL: https://github.com/stevepolitodesign/approaches-to-confirming-user-emails-in-rails
- Owner: stevepolitodesign
- Created: 2023-01-15T14:31:04.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2023-01-18T14:01:49.000Z (over 1 year ago)
- Last Synced: 2023-03-06T00:21:52.183Z (over 1 year ago)
- Language: Ruby
- Size: 32.2 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Approaches to Confirming User Emails in Rails
## Approach 1
Use [signed_id][] without an expiration and purpose.
**Advantages**
- Links cannot be tampered with.
- Links are obfuscated.**Disadvantages**
- Links can be used multiple times.
- Links do not expire.
- Links are not restricted.```ruby
class UsersController < ApplicationController
def create
@user = User.new(user_params)if @user.save
# Generate Signed ID.
signed_id = @user.signed_id# Share this URL in a Mailer.
# edit_confirmation_url(signed_id)
# https://www.example.com/confirmations/{signed_id}/edit
end
end
end
``````ruby
Rails.application.routes.draw do
# Add a route for processing confirmations. Use the `signed_id` as the identifier.
resources :confirmations, only: :edit, param: :signed_id
end
``````ruby
class ConfirmationsController < ApplicationController
def edit
# Find the unconfirmed user by their Signed ID so that they can be confirmed.
@user = User.unconfirmed.find_signed!(params[:signed_id])
@user.update!(confirmed_at: Time.current)
end
end
```## Approach 2
Use [signed_id][] with an expiration and purpose.
**Advantages**
- Links cannot be tampered with.
- Links are obfuscated.
- Links expire.
- Links are restricted.**Disadvantages**
- Links can be used multiple times before expiring.
```ruby
class UsersController < ApplicationController
def create
@user = User.new(user_params)if @user.save
# Generate Signed ID with an expiration and purpose.
signed_id = @user.signed_id expires_in: 15.minutes, purpose: :confirmation# Share this URL in a Mailer.
# edit_confirmation_url(signed_id)
# https://www.example.com/confirmations/{signed_id}/edit
end
end
end
``````ruby
Rails.application.routes.draw do
# Add a route for processing confirmations. Use the `signed_id` as the identifier.
resources :confirmations, only: :edit, param: :signed_id
end
``````ruby
class ConfirmationsController < ApplicationController
def edit
# Find the unconfirmed user by their Signed ID with a purpose so that they can be confirmed.
@user = User.unconfirmed.find_signed!(params[:signed_id], purpose: :confirmation)
@user.update!(confirmed_at: Time.current)
end
end
```## Approach 3
Use [signed_id][] with an expiration and purpose on a proxy record.
**Advantages**
- Links cannot be tampered with.
- Links are obfuscated.
- Links expire.
- Links are restricted.
- Links cannot be used multiple times.
- Links can be revoked.
- Can be applied to multiple records.**Disadvantages**
- Adds an additional database table.
- Can no longer scope query to unconfirmed accounts.```ruby
class CreateConfirmations < ActiveRecord::Migration[7.0]
def change
create_table :confirmations do |t|
t.references :confirmable, polymorphic: true, null: falset.timestamps
end
end
end
``````ruby
class Confirmation < ApplicationRecord
belongs_to :confirmable, polymorphic: true
end
``````ruby
class User < ApplicationRecord
has_many :confirmations, as: :confirmable
end
``````ruby
class UsersController < ApplicationController
def create
@user = User.new(user_params)if @user.save
# Create a new confirmation record associated with the user.
confirmation = @user.confirmations.create!
# Generate Signed ID with an expiration and purpose.
signed_id = confirmation.signed_id expires_in: 15.minutes, purpose: :confirmation# Share this URL in a Mailer.
# edit_confirmation_url(signed_id)
# https://www.example.com/confirmations/{signed_id}/edit
end
end
end
``````ruby
Rails.application.routes.draw do
# Add a route for processing confirmations. Use the `signed_id` as the identifier.
resources :confirmations, only: :edit, param: :signed_id
end
``````ruby
class ConfirmationsController < ApplicationController
def edit
# Find the unconfirmed user via a proxy so that they can be confirmed.
@confirmation = Confirmation.find_signed!(params[:signed_id], purpose: :confirmation)
@confirmation.confirmable.update!(confirmed_at: Time.current)# Destroy any existing confirmations for that record so they cannot be used.
@confirmation.confirmable.confirmations.destroy_all!
end
end
```### Modifications
Limit the number of confirmations a record can be associated with to one.
```ruby
class AddUniqueConstraintToConfirmations < ActiveRecord::Migration[7.0]
def change
add_index :confirmations, [:confirmable_id, :confirmable_type], unique: true
end
end
``````ruby
class Confirmation < ApplicationRecord
belongs_to :confirmable, polymorphic: truevalidates :confirmable_type, uniqueness: { scope: :confirmable_id }
end
``````ruby
class User < ApplicationRecord
has_one :confirmation, as: :confirmable
end
``````ruby
class UsersController < ApplicationController
def create
@user = User.new(user_params)if @user.save
# Create a new confirmation record associated with the user.
confirmation = @user.create_confirmation!
# Generate Signed ID with an expiration and purpose.
signed_id = confirmation.signed_id expires_in: 15.minutes, purpose: :confirmation# Share this URL in a Mailer.
# edit_confirmation_url(signed_id)
# https://www.example.com/confirmations/{signed_id}/edit
end
end
end
``````ruby
class ConfirmationsController < ApplicationController
def edit
# Find the unconfirmed user via a proxy so that they can be confirmed.
@confirmation = Confirmation.find_signed!(params[:signed_id], purpose: :confirmation)
@confirmation.confirmable.update!(confirmed_at: Time.current)# Destroy the confirmation.
@confirmation.destroy!
end
end
```[signed_id]: https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html#method-i-signed_id