diff --git a/Gemfile b/Gemfile index 463e5c3..390be6e 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 391d20f..ac3e8b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/javascripts/tests.coffee b/app/assets/javascripts/tests.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/tests.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/ diff --git a/app/assets/javascripts/user_sessions.coffee b/app/assets/javascripts/user_sessions.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/user_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/ diff --git a/app/assets/stylesheets/tests.scss b/app/assets/stylesheets/tests.scss new file mode 100644 index 0000000..7e46f05 --- /dev/null +++ b/app/assets/stylesheets/tests.scss @@ -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/ diff --git a/app/assets/stylesheets/user_sessions.scss b/app/assets/stylesheets/user_sessions.scss new file mode 100644 index 0000000..69017e2 --- /dev/null +++ b/app/assets/stylesheets/user_sessions.scss @@ -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/ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e..32e77eb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb new file mode 100644 index 0000000..4faf46f --- /dev/null +++ b/app/controllers/tests_controller.rb @@ -0,0 +1,7 @@ +class TestsController < ApplicationController + def index + end + + def show + end +end diff --git a/app/controllers/user_sessions_controller.rb b/app/controllers/user_sessions_controller.rb new file mode 100644 index 0000000..90a3384 --- /dev/null +++ b/app/controllers/user_sessions_controller.rb @@ -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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f072718..2058216 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -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 diff --git a/app/models/user_session.rb b/app/models/user_session.rb new file mode 100644 index 0000000..a8ab134 --- /dev/null +++ b/app/models/user_session.rb @@ -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 diff --git a/app/views/application/_navbar.html.slim b/app/views/application/_navbar.html.slim index 9a2531a..9a52d66 100644 --- a/app/views/application/_navbar.html.slim +++ b/app/views/application/_navbar.html.slim @@ -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 diff --git a/app/views/tests/index.html.slim b/app/views/tests/index.html.slim new file mode 100644 index 0000000..0f02584 --- /dev/null +++ b/app/views/tests/index.html.slim @@ -0,0 +1 @@ += 'tests#index' diff --git a/app/views/tests/show.html.slim b/app/views/tests/show.html.slim new file mode 100644 index 0000000..9606ca0 --- /dev/null +++ b/app/views/tests/show.html.slim @@ -0,0 +1 @@ += 'tests#show' diff --git a/app/views/user_sessions/create.html.slim b/app/views/user_sessions/create.html.slim new file mode 100644 index 0000000..e69de29 diff --git a/app/views/user_sessions/new.html.slim b/app/views/user_sessions/new.html.slim new file mode 100644 index 0000000..f0c833b --- /dev/null +++ b/app/views/user_sessions/new.html.slim @@ -0,0 +1,6 @@ += simple_form_for @user_session do |f| + .form-inputs + = f.input :username + = f.input :password + .form-actions + = f.button :submit diff --git a/config/routes.rb b/config/routes.rb index 0b5bce2..de7e608 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20150710040354_create_user_sessions.rb b/db/migrate/20150710040354_create_user_sessions.rb new file mode 100644 index 0000000..4e1eae4 --- /dev/null +++ b/db/migrate/20150710040354_create_user_sessions.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 0f9d4cc..ec23418 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb new file mode 100644 index 0000000..d12643c --- /dev/null +++ b/spec/controllers/application_controller_spec.rb @@ -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 diff --git a/spec/controllers/user_sessions_controller_spec.rb b/spec/controllers/user_sessions_controller_spec.rb new file mode 100644 index 0000000..deaa1e9 --- /dev/null +++ b/spec/controllers/user_sessions_controller_spec.rb @@ -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 diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index fc922c2..9182884 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -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 diff --git a/spec/factories/user_factory.rb b/spec/factories/user_factory.rb index a10dea6..ee760d9 100644 --- a/spec/factories/user_factory.rb +++ b/spec/factories/user_factory.rb @@ -1,4 +1,3 @@ - FactoryGirl.define do factory :user do username { Faker::Internet.user_name } diff --git a/spec/factories/user_session_factory.rb b/spec/factories/user_session_factory.rb new file mode 100644 index 0000000..71479b4 --- /dev/null +++ b/spec/factories/user_session_factory.rb @@ -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 diff --git a/spec/models/user_session_spec.rb b/spec/models/user_session_spec.rb new file mode 100644 index 0000000..d293091 --- /dev/null +++ b/spec/models/user_session_spec.rb @@ -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 diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb index 58409cb..690d478 100644 --- a/spec/support/simplecov.rb +++ b/spec/support/simplecov.rb @@ -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