Parcourir la source

Add login with 'remember me' functionality

Users are required to log in to see anything but the login form.
Frans Bergman il y a 7 ans
Parent
commit
559fcc8698

+ 3 - 0
app/assets/javascripts/sessions.coffee

@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/

+ 21 - 0
app/assets/stylesheets/sessions.scss

@@ -0,0 +1,21 @@
+// Place all the styles related to the Sessions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
+
+.login-panel {
+  margin-top: 30%;
+}
+
+.checkbox {
+  margin-top: -10px;
+  margin-bottom: 10px;
+  span {
+    margin-left: 20px;
+    font-weight: normal;
+  }
+}
+
+#session_remember_me {
+  width: auto;
+  margin-left: 0;
+}

+ 12 - 0
app/controllers/application_controller.rb

@@ -1,3 +1,15 @@
 class ApplicationController < ActionController::Base
   protect_from_forgery with: :exception
+  include SessionsHelper
+
+  before_action :logged_in_user
+
+  private
+    def logged_in_user
+      unless logged_in? || controller_name == 'sessions'
+        store_location
+        flash[:danger] = "Please log in."
+        redirect_to login_path
+      end
+    end
 end

+ 22 - 0
app/controllers/sessions_controller.rb

@@ -0,0 +1,22 @@
+class SessionsController < ApplicationController
+  def new
+    redirect_to home_path if logged_in?
+  end
+
+  def create
+    @user = User.find_by(login: params[:session][:login].downcase)
+    if @user && @user.authenticate(params[:session][:password])
+      log_in @user
+      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
+      redirect_back_or home_url
+    else
+      flash.now[:danger] = 'Invalid login/password combination'
+      render 'new'
+    end
+  end
+
+  def destroy
+    log_out if logged_in?
+    redirect_to login_url
+  end
+end

+ 62 - 0
app/helpers/sessions_helper.rb

@@ -0,0 +1,62 @@
+module SessionsHelper
+
+  # Logs in the given user
+  def log_in(user)
+    session[:user_id] = user.id
+  end
+
+  # Remembers a user in a persistent session.
+  def remember(user)
+    user.remember
+    cookies.permanent.signed[:user_id] = user.id
+    cookies.permanent[:remember_token] = user.remember_token
+  end
+
+  # Returns true if the given user is the current user.
+  def current_user?(user)
+    user == current_user
+  end
+
+  # Returns the current logged-in user (if any).
+  def current_user
+    if (user_id = session[:user_id])
+      @current_user ||= User.find_by(id: user_id)
+    elsif (user_id = cookies.signed[:user_id])
+      user = User.find_by(id: user_id)
+      if user && user.authenticated?(:remember, cookies[:remember_token])
+        log_in user
+        @current_user = user
+      end
+    end
+  end
+
+  # Returns true if the user is logged in, false otherwise.
+  def logged_in?
+    !current_user.nil?
+  end
+
+  # Forgets a persistent session.
+  def forget(user)
+    user.forget
+    cookies.delete(:user_id)
+    cookies.delete(:remember_token)
+  end
+
+  # Logs out the current user.
+  def log_out
+    forget current_user
+    session.delete(:user_id)
+    @current_user = nil
+  end
+
+  # Redirects to stored location (or to the default).
+  def redirect_back_or(default)
+    redirect_to(session[:forwarding_url] || default)
+    session.delete(:forwarding_url)
+  end
+
+  # Stores the URL trying to be accessed.
+  def store_location
+    session[:forwarding_url] = request.original_url if request.get?
+  end
+end

+ 23 - 0
app/models/user.rb

@@ -1,4 +1,5 @@
 class User < ApplicationRecord
+  attr_accessor :remember_token
 
   validates :name, presence: true
 
@@ -21,4 +22,26 @@ class User < ApplicationRecord
                                                   BCrypt::Engine.cost
     BCrypt::Password.create(string, cost: cost)
   end
+
+  # Returns a random token.
+  def User.new_token
+    SecureRandom.urlsafe_base64
+  end
+
+  def remember
+    self.remember_token = User.new_token
+    update_attribute(:remember_digest, User.digest(remember_token))
+  end
+
+  # Returns true if the given token matches the digest.
+  def authenticated?(attribute, token)
+    digest = send("#{attribute}_digest")
+    return false if digest.nil?
+    BCrypt::Password.new(digest).is_password?(token)
+  end
+
+  # Forgets a user.
+  def forget
+    update_attribute(:remember_digest, nil)
+  end
 end

+ 2 - 2
app/views/layouts/_navigation.html.erb

@@ -16,14 +16,14 @@
             </a>
             <ul class="dropdown-menu dropdown-user">
                 <li>
-                  <%= link_to fa_icon("user fw", text: "User Profile"), "#" %>
+                  <%= link_to fa_icon("user fw", text: "User Profile"), current_user %>
                 </li>
                 <li>
                   <%= link_to fa_icon("gear fw", text: "Settings"), "#" %>
                 </li>
                 <li class="divider"></li>
                 <li>
-                  <%= link_to fa_icon("sign-out fw", text: "Logout"), "#" %>
+                  <%= link_to fa_icon("sign-out fw", text: "Logout"), logout_path, method: :delete %>
                 </li>
             </ul>
         </li>

+ 3 - 3
app/views/layouts/application.html.erb

@@ -11,10 +11,10 @@
 
   <body>
     <div id="wrapper">
-      <%= render 'layouts/navigation' %>
+      <%= render 'layouts/navigation' if logged_in? %>
 
       <!-- Page Content -->
-      <div id="page-wrapper">
+      <div id="<%= 'page-wrapper' if logged_in?%>">
           <div class="container-fluid">
               <div class="row">
                   <div class="col-lg-12">
@@ -27,7 +27,7 @@
               </div>
           </div>
       </div>
-      <%= render 'layouts/footer' %>
+      <%= render 'layouts/footer' if logged_in? %>
     </div>
   </body>
 </html>

+ 32 - 0
app/views/sessions/new.html.erb

@@ -0,0 +1,32 @@
+<% provide(:title, "Log in") %>
+
+<div class="row">
+  <div class="col-md-4 col-md-offset-4">
+    <div class="login-panel panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">Please Sign In</h3>
+      </div>
+      <div class="panel-body">
+        <%= form_for(:session, url: login_path) do |f| %>
+
+          <div class="form-group">
+            <%= f.label :username %>
+            <%= f.text_field :login, class: 'form-control', autofocus: 'autofocus' %>
+          </div>
+
+          <div class="form-group">
+            <%= f.label :password %>
+            <%= f.password_field :password, class: 'form-control' %>
+          </div>
+
+          <%= f.label :remember_me, class: "checkbox inline" do %>
+            <%= f.check_box :remember_me %>
+            <span>Remember me on this computer</span>
+          <% end %>
+
+          <%= f.submit "Log in", class: "btn btn-lg btn-block btn-success" %>
+        <% end %>
+      </div>
+    </div>
+  </div>
+</div>

+ 5 - 3
config/routes.rb

@@ -1,10 +1,12 @@
 Rails.application.routes.draw do
-  get 'users/new'
-
-  root 'static_pages#home'
+  root 'sessions#new'
 
   get '/home', to: 'static_pages#home'
   get '/about', to: 'static_pages#about'
 
+  get    '/login',   to: 'sessions#new'
+  post   '/login',   to: 'sessions#create'
+  delete '/logout',  to: 'sessions#destroy'
+
   resources :users
 end

+ 5 - 0
db/migrate/20171217153703_add_remember_digest_to_users.rb

@@ -0,0 +1,5 @@
+class AddRememberDigestToUsers < ActiveRecord::Migration[5.1]
+  def change
+    add_column :users, :remember_digest, :string
+  end
+end

+ 2 - 1
db/schema.rb

@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20171217122327) do
+ActiveRecord::Schema.define(version: 20171217153703) do
 
   create_table "users", force: :cascade do |t|
     t.string "name"
@@ -19,6 +19,7 @@ ActiveRecord::Schema.define(version: 20171217122327) do
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
     t.string "password_digest"
+    t.string "remember_digest"
     t.index ["login"], name: "index_users_on_login", unique: true
   end
 

+ 9 - 0
test/controllers/sessions_controller_test.rb

@@ -0,0 +1,9 @@
+require 'test_helper'
+
+class SessionsControllerTest < ActionDispatch::IntegrationTest
+  test "should get new" do
+    get login_path
+    assert_response :success
+  end
+
+end

+ 5 - 0
test/controllers/static_pages_controller_test.rb

@@ -1,6 +1,11 @@
 require 'test_helper'
 
 class StaticPagesControllerTest < ActionDispatch::IntegrationTest
+
+  def setup
+    log_in_as users(:daniel)
+  end
+
   test "should get home" do
     get home_url
     assert_response :success

+ 0 - 4
test/controllers/users_controller_test.rb

@@ -1,9 +1,5 @@
 require 'test_helper'
 
 class UsersControllerTest < ActionDispatch::IntegrationTest
-  test "should get new" do
-    get users_new_url
-    assert_response :success
-  end
 
 end

+ 12 - 2
test/integration/site_layout_test.rb

@@ -1,11 +1,21 @@
 require 'test_helper'
 
 class SiteLayoutTest < ActionDispatch::IntegrationTest
-  test "layout links" do
-    get root_path
+
+  test "logged in layout links" do
+    log_in_as users(:daniel)
+    get home_path
     assert_template 'static_pages/home'
     assert_select "a[href=?]", root_path
     assert_select "a[href=?]", home_path
     assert_select "a[href=?]", about_path
   end
+
+  test "logged out" do
+    get home_path
+    assert_redirected_to login_url
+    follow_redirect!
+    assert_template 'sessions/new'
+    assert_select "nav", count: 0
+  end
 end

+ 49 - 0
test/integration/users_login_test.rb

@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class UsersLoginTest < ActionDispatch::IntegrationTest
+  def setup
+    @user = users(:daniel)
+  end
+
+  test "login with invalid information" do
+    get login_path
+    assert_template 'sessions/new'
+    post login_path, params: { session: { login: "", password: "" } }
+    assert_template 'sessions/new'
+    assert_not flash.empty?
+    get root_path
+    assert flash.empty?
+  end
+
+  test "login with valid information followed by logout" do
+    get login_path
+    post login_path, params: { session: { login:    @user.login,
+                                          password: 'password' } }
+    assert is_logged_in?
+    assert_redirected_to home_url
+    follow_redirect!
+    assert_template 'static_pages/home'
+    assert_select "a[href=?]", login_path, count: 0
+    assert_select "a[href=?]", logout_path
+    assert_select "a[href=?]", user_path(@user)
+    delete logout_path
+    assert_not is_logged_in?
+    assert_redirected_to login_url
+    # Simulate a user clicking logout in a second window.
+    delete logout_path
+    assert_redirected_to login_path
+  end
+
+  test "login with remembering" do
+    log_in_as(@user, remember_me: '1')
+    assert_equal cookies['remember_token'], assigns(:user).remember_token
+  end
+
+  test "login without remembering" do
+    # Log in to set the cookie.
+    log_in_as(@user, remember_me: '1')
+    # Log in again and verify that the cookie is deleted.
+    log_in_as(@user, remember_me: '0')
+    assert_empty cookies['remember_token']
+  end
+end

+ 19 - 1
test/test_helper.rb

@@ -5,5 +5,23 @@ class ActiveSupport::TestCase
   # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
   fixtures :all
 
-  # Add more helper methods to be used by all tests here...
+  # Returns true if a test user is logged in.
+  def is_logged_in?
+    !session[:user_id].nil?
+  end
+
+  # Log in as a particular user.
+  def log_in_as(user)
+    session[:user_id] = user.id
+  end
+end
+
+class ActionDispatch::IntegrationTest
+
+  # Log in as a particular user.
+  def log_in_as(user, password: 'password', remember_me: '1')
+    post login_path, params: { session: { login: user.login,
+                                          password: password,
+                                          remember_me: remember_me } }
+  end
 end