Add basic sessions generator (#52328)
* Add basic sessions generator * Excess CR * Use required fields * Add sessions generator test * Fix generated migration * Test migration content * Appease rubocop * Add CHANGELOG
This commit is contained in:
parent
3ce3a4e94c
commit
7b1ceb7659
@ -1,3 +1,22 @@
|
||||
* Add sessions generator to give a basic start to an authentication system using database-tracked sessions.
|
||||
|
||||
|
||||
# Generate with...
|
||||
bin/rails sessions
|
||||
|
||||
# Generated files
|
||||
app/models/current.rb
|
||||
app/models/user.rb
|
||||
app/models/session.rb
|
||||
app/controllers/sessions_controller.rb
|
||||
app/views/sessions/new.html.erb
|
||||
db/migrate/xxxxxxx_create_users.rb
|
||||
db/migrate/xxxxxxx_create_sessions.rb
|
||||
|
||||
|
||||
*DHH*
|
||||
|
||||
|
||||
* Add not-null type modifier to migration attributes.
|
||||
|
||||
|
||||
|
6
railties/lib/rails/generators/rails/sessions/USAGE
Normal file
6
railties/lib/rails/generators/rails/sessions/USAGE
Normal file
@ -0,0 +1,6 @@
|
||||
Description:
|
||||
Generates a basic sessions system with user authentication.
|
||||
|
||||
Example:
|
||||
`bin/rails generate sessions`
|
||||
|
@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
class SessionsGenerator < Base # :nodoc:
|
||||
def create_session_files
|
||||
template "models/session.rb", File.join("app/models/session.rb")
|
||||
template "models/user.rb", File.join("app/models/user.rb")
|
||||
template "models/current.rb", File.join("app/models/current.rb")
|
||||
|
||||
template "controllers/sessions_controller.rb", File.join("app/controllers/sessions_controller.rb")
|
||||
template "controllers/concerns/authentication.rb", File.join("app/controllers/concerns/authentication.rb")
|
||||
|
||||
template "views/sessions/new.html.erb", File.join("app/views/sessions/new.html.erb")
|
||||
end
|
||||
|
||||
def configure_application
|
||||
gsub_file "app/controllers/application_controller.rb", /(class ApplicationController < ActionController::Base)/, "\\1\n include Authentication"
|
||||
route "resource :session"
|
||||
end
|
||||
|
||||
def enable_bcrypt
|
||||
# FIXME: Make more resilient in case the default comment has been removed
|
||||
gsub_file "Gemfile", /# gem "bcrypt"/, 'gem "bcrypt"'
|
||||
execute_command :bundle, ""
|
||||
end
|
||||
|
||||
def add_migrations
|
||||
generate "migration CreateUsers email_address:string!:uniq password_digest:string! --force"
|
||||
generate "migration CreateSessions user:references token:token ip_address:string user_agent:string --force"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
64
railties/lib/rails/generators/rails/sessions/templates/controllers/concerns/authentication.rb
Normal file
64
railties/lib/rails/generators/rails/sessions/templates/controllers/concerns/authentication.rb
Normal file
@ -0,0 +1,64 @@
|
||||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :require_authentication
|
||||
helper_method :authenticated?
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def allow_unauthenticated_access(**options)
|
||||
skip_before_action :require_authentication, **options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def authenticated?
|
||||
Current.session.present?
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
resume_session || request_authentication
|
||||
end
|
||||
|
||||
|
||||
def resume_session
|
||||
if session = find_session_by_cookie
|
||||
set_current_session session
|
||||
end
|
||||
end
|
||||
|
||||
def find_session_by_cookie
|
||||
if token = cookies.signed[:session_token]
|
||||
Session.find_by(token: token)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
redirect_to new_session_url
|
||||
end
|
||||
|
||||
def after_authentication_url
|
||||
session.delete(:return_to_after_authenticating) || root_url
|
||||
end
|
||||
|
||||
|
||||
def start_new_session_for(user)
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
||||
set_current_session session
|
||||
end
|
||||
end
|
||||
|
||||
def set_current_session(session)
|
||||
Current.session = session
|
||||
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
|
||||
end
|
||||
|
||||
def terminate_session
|
||||
Current.session.destroy
|
||||
cookies.delete(:session_token)
|
||||
end
|
||||
end
|
||||
|
22
railties/lib/rails/generators/rails/sessions/templates/controllers/sessions_controller.rb
Normal file
22
railties/lib/rails/generators/rails/sessions/templates/controllers/sessions_controller.rb
Normal file
@ -0,0 +1,22 @@
|
||||
class SessionsController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
|
||||
|
||||
def new
|
||||
redirect_to root_url if authenticated?
|
||||
end
|
||||
|
||||
def create
|
||||
if user = User.authenticate_by(params.permit(:email_address, :password))
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
else
|
||||
redirect_to new_session_url, alert: "Try another email address or password."
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
terminate_session
|
||||
redirect_to new_session_url
|
||||
end
|
||||
end
|
@ -0,0 +1,5 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
end
|
||||
|
@ -0,0 +1,5 @@
|
||||
class Session < ApplicationRecord
|
||||
has_secure_token
|
||||
belongs_to :user
|
||||
end
|
||||
|
@ -0,0 +1,4 @@
|
||||
class User < ApplicationRecord
|
||||
has_secure_password validations: false
|
||||
has_many :sessions, dependent: :destroy
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
<%% if alert = flash[:alert] %>
|
||||
<div style="color:red"><%%= alert %></div>
|
||||
<%% end %>
|
||||
|
||||
<%%= form_with url: session_url do |form| %>
|
||||
<div>
|
||||
<%%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %>
|
||||
</div>
|
||||
|
||||
<%%= form.submit "Sign in" %>
|
||||
<%% end %>
|
53
railties/test/generators/sessions_generator_test.rb
Normal file
53
railties/test/generators/sessions_generator_test.rb
Normal file
@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "generators/generators_test_helper"
|
||||
require "rails/generators/rails/app/app_generator"
|
||||
require "rails/generators/rails/sessions/sessions_generator"
|
||||
|
||||
class SessionsGeneratorTest < Rails::Generators::TestCase
|
||||
include GeneratorsTestHelper
|
||||
|
||||
def setup
|
||||
Rails.application = TestApp::Application
|
||||
Rails.application.config.root = Pathname(destination_root)
|
||||
end
|
||||
|
||||
def teardown
|
||||
Rails.application = Rails.application.instance
|
||||
end
|
||||
|
||||
def test_session_generator
|
||||
self.class.tests Rails::Generators::AppGenerator
|
||||
run_generator([destination_root])
|
||||
|
||||
self.class.tests Rails::Generators::SessionsGenerator
|
||||
run_generator
|
||||
|
||||
assert_file "app/models/user.rb"
|
||||
assert_file "app/models/current.rb"
|
||||
assert_file "app/models/session.rb"
|
||||
assert_file "app/controllers/sessions_controller.rb"
|
||||
assert_file "app/controllers/concerns/authentication.rb"
|
||||
assert_file "app/views/sessions/new.html.erb"
|
||||
|
||||
assert_file "app/controllers/application_controller.rb" do |content|
|
||||
assert_match(/include Authentication/, content)
|
||||
end
|
||||
|
||||
assert_file "Gemfile" do |content|
|
||||
assert_match(/\ngem "bcrypt"/, content)
|
||||
end
|
||||
|
||||
assert_file "config/routes.rb" do |content|
|
||||
assert_match(/resource :session/, content)
|
||||
end
|
||||
|
||||
assert_migration "db/migrate/create_sessions.rb" do |content|
|
||||
assert_match(/t.references :user, null: false, foreign_key: true/, content)
|
||||
end
|
||||
|
||||
assert_migration "db/migrate/create_users.rb" do |content|
|
||||
assert_match(/t.string :password_digest, null: false/, content)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user