1
0
Fork 0

Merge branch 'atomaka/feature/sessions' into 'master'

Add user sessions

Although rails provides built-in user sessions, this allows us to add additional fields to the session model. For example, we can now track the user agent and IP address of all sessions associated with a user.  Long term, this allows us to do neat things like session revocation (by both user and admin) and sudo mode.

See merge request !5
This commit is contained in:
Andrew Tomaka 2015-07-10 14:59:45 -04:00
commit f91390baf6
26 changed files with 392 additions and 10 deletions

View File

@ -29,6 +29,7 @@ group :development do
gem 'better_errors'
gem 'quiet_assets'
gem 'metric_fu'
gem 'binding_of_caller'
end
group :development, :test do

View File

@ -52,6 +52,8 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.5.1)
autoprefixer-rails (>= 5.0.0.1)
sass (>= 3.3.0)
@ -95,6 +97,7 @@ GEM
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
database_cleaner (1.4.1)
debug_inspector (0.0.2)
diff-lcs (1.2.5)
docile (1.1.5)
domain_name (0.5.24)
@ -361,6 +364,7 @@ PLATFORMS
DEPENDENCIES
bcrypt
better_errors
binding_of_caller
bootstrap-sass
bullet
capybara

View File

@ -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/

View File

@ -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/

View File

@ -0,0 +1,3 @@
// Place all the styles related to the Tests controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -0,0 +1,3 @@
// Place all the styles related to the UserSessions controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -1,5 +1,21 @@
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
helper_method :current_user
helper_method :current_session
helper_method :logged_in?
private
def current_user
@current_user ||= User.find(current_session[:user_id]) if current_session
end
def current_session
@current_session ||= UserSession.authenticate(cookies[:user_session])
end
def logged_in?
!!current_user
end
end

View File

@ -0,0 +1,7 @@
class TestsController < ApplicationController
def index
end
def show
end
end

View File

@ -0,0 +1,42 @@
class UserSessionsController < ApplicationController
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(user_session_params)
user = User.find_by_username(params[:user_session][:username])
if authenticate_user?(user)
create_user_session(user)
redirect_to root_path
else
render :new
end
end
def destroy
cookies.permanent[:user_session] = nil
current_session.destroy if current_session
redirect_to root_path
end
private
def user_session_params
params.require(:user_session).permit(:username, :password)
end
def authenticate_user?(user)
user && user.authenticate(params[:user_session][:password])
end
def create_user_session(user)
user_session = UserSession.new_by_user(user, request.env)
cookies.permanent[:user_session] = user_session.key
end
end

View File

@ -7,7 +7,7 @@ class UsersController < ApplicationController
@user = User.new(user_params)
if @user.save
render :create
redirect_to signin_path
else
render :new
end

View File

@ -0,0 +1,27 @@
class UserSession < ActiveRecord::Base
belongs_to :user
before_validation :set_unique_key
attr_accessor :username, :password
def self.authenticate(key)
self.find_by_key(key)
end
def self.new_by_user(user, env)
user_session = UserSession.new(
user: user,
user_agent: env['HTTP_USER_AGENT'],
ip: env['REMOTE_ADDR']
)
user_session.save
user_session
end
private
def set_unique_key
self.key = SecureRandom.urlsafe_base64(32)
end
end

View File

@ -9,5 +9,9 @@
= link_to 'Creddit', root_path, class: 'navbar-brand'
.collapse.navbar-collapse
ul.nav.navbar-nav.navbar-right
li= link_to 'Create Account', new_user_path
- if logged_in?
li= link_to 'Sign Out', signout_path
- else
li= link_to 'Create Account', signup_path
li= link_to 'Sign In', signin_path

View File

@ -0,0 +1 @@
= 'tests#index'

View File

@ -0,0 +1 @@
= 'tests#show'

View File

View File

@ -0,0 +1,6 @@
= simple_form_for @user_session do |f|
.form-inputs
= f.input :username
= f.input :password
.form-actions
= f.button :submit

View File

@ -1,5 +1,12 @@
Rails.application.routes.draw do
resources :users, only: [:new, :create]
resources :tests, only: [:index, :show]
root to: 'users#new'
get 'signup', to: 'users#new', as: :signup
get 'signin', to: 'user_sessions#new', as: :signin
get 'signout', to: 'user_sessions#destroy', as: :signout
resources :users, only: [:new, :create]
resources :user_sessions, only: [:new, :create, :destroy]
root to: 'tests#index'
end

View File

@ -0,0 +1,13 @@
class CreateUserSessions < ActiveRecord::Migration
def change
create_table :user_sessions do |t|
t.references :user, index: true, foreign_key: true
t.string :key
t.string :user_agent
t.string :ip
t.datetime :accessed_at
t.timestamps null: false
end
end
end

View File

@ -11,7 +11,19 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20150708191201) do
ActiveRecord::Schema.define(version: 20150710040354) do
create_table "user_sessions", force: :cascade do |t|
t.integer "user_id"
t.string "key"
t.string "user_agent"
t.string "ip"
t.datetime "accessed_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "user_sessions", ["user_id"], name: "index_user_sessions_on_user_id"
create_table "users", force: :cascade do |t|
t.string "username"

View File

@ -0,0 +1,54 @@
require 'rails_helper'
describe ApplicationController, type: :controller do
let!(:user) { create(:user) }
let!(:user_session) { create(:user_session, user: user) }
describe '#logged_in?' do
context 'when logged in' do
before(:each) { request.cookies['user_session'] = user_session.key }
it 'should return true' do
expect(controller.send(:logged_in?)).to be(true)
end
end
context 'when not logged in' do
it 'should return false' do
expect(controller.send(:logged_in?)).to be(false)
end
end
end
describe '#current_user' do
context 'when logged in' do
before(:each) { request.cookies['user_session'] = user_session.key }
it 'should return the current user' do
expect(controller.send(:current_user)).to eq(user)
end
end
context 'when not logged in' do
it 'should return nil' do
expect(controller.send(:current_user)).to be_nil
end
end
end
describe '#current_session' do
context 'when logged in' do
before(:each) { request.cookies['user_session'] = user_session.key }
it 'should return the curren session' do
expect(controller.send(:current_session)).to eq(user_session)
end
end
context 'when not logged in' do
it 'should return nil' do
expect(controller.send(:current_session)).to be_nil
end
end
end
end

View File

@ -0,0 +1,109 @@
require 'rails_helper'
describe UserSessionsController, type: :controller do
describe '#new' do
it 'should render :new' do
get :new
expect(response).to render_template(:new)
end
it 'should set a new Session to @session' do
get :new
expect(assigns(:user_session)).to be_a_new(UserSession)
end
end
describe '#create' do
let!(:user) { create(:user) }
let(:data) do
{
username: user.username,
password: user.password
}
end
context 'with valid credentials' do
it 'should create a user session' do
expect do
post :create, user_session: data
end.to change(UserSession, :count).by(1)
end
it 'should create a correct user_session cookie' do
post :create, user_session: data
expect(response.cookies['user_session']).to eq(UserSession.first.key)
end
it 'should redirect to the home page' do
post :create, user_session: data
expect(response).to redirect_to(root_path)
end
end
context 'with invalid credentials' do
it 'should not create a user session with a blank username' do
data['username'] = ''
expect do
post :create, user_session: data
end.to change(UserSession, :count).by(0)
end
it 'should not create a user session invalid credentials' do
data['password'] = 'badpassword'
expect do
post :create, user_session: data
end.to change(UserSession, :count).by(0)
end
it 'should render :new' do
data['username'] = ''
post :create, user_session: data
expect(response).to render_template(:new)
end
end
end
describe '#delete' do
let!(:user) { create(:user) }
let(:data) do
{
username: user.username,
password: user.password
}
end
context 'with a valid session' do
let(:user_session) { create(:user_session) }
it 'should delete the user_session cookie' do
request.cookies['user_session'] = user_session.key
delete :destroy
expect(response.cookies['user_session']).to be_nil
end
it 'should delete the UserSession' do
request.cookies['user_session'] = user_session.key
allow_any_instance_of(ApplicationController)
.to receive(:current_session).and_return(user_session)
expect { delete :destroy }.to change(UserSession, :count).by(-1)
end
it 'should redirect to the root' do
delete :destroy
expect(response).to redirect_to(root_path)
end
end
end
end

View File

@ -29,7 +29,11 @@ describe UsersController, type: :controller do
expect { post :create, user: data }.to change(User, :count).by(1)
end
it 'should redirect to the login page'
it 'should redirect to the login page' do
post :create, user: data
expect(response).to redirect_to signin_path
end
end
context 'with invalid data' do

View File

@ -1,4 +1,3 @@
FactoryGirl.define do
factory :user do
username { Faker::Internet.user_name }

View File

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :user_session do
user
user_agent { Faker::Lorem.sentence }
ip { Faker::Internet.ip_v4_address }
end
end

View File

@ -0,0 +1,52 @@
require 'rails_helper'
RSpec.describe UserSession, type: :model do
describe '.active' do
context 'with valid session' do
let(:user_session) { create(:user_session) }
context 'with correct key' do
it 'should find the correct session' do
expect(UserSession.authenticate(user_session.key)).to eq(user_session)
end
end
context 'with invalid key' do
it 'should not find a session' do
expect(UserSession.authenticate('aaaaaa')).to be_nil
end
end
end
end
describe '.new_by_user' do
let(:user) { build(:user) }
let(:env) do
{
'HTTP_USER_AGENT': 'Test User Agent',
'REMOTE_ADDR': '192.168.1.1'
}
end
context 'with valid user and environment' do
let(:user_session) { UserSession.new_by_user(user, env) }
it 'should create a new session' do
# duplicated user_session creation for simplecov's benefit
expect(UserSession.new_by_user(user, env)).to be_a(UserSession)
end
it 'should set the correct user' do
expect(user_session.user).to eq(user)
end
it 'should set the correct user agent' do
expect(user_session.user_agent).to eq(env['HTTP_USER_AGENT'])
end
it 'should set the correct IP address' do
expect(user_session.ip).to eq(env['REMOTE_ADDR'])
end
end
end
end

View File

@ -2,4 +2,8 @@ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
SimpleCov::Formatter::HTMLFormatter,
SimpleCov::Formatter::MetricFu
]
SimpleCov.start
SimpleCov.start do
add_filter 'spec/'
add_filter 'config/'
add_filter 'vendor/'
end