Skip to content

Commit

Permalink
Merge pull request #42 from JonathanPorta/feature-lockdown-api
Browse files Browse the repository at this point in the history
Basic user authentication and creation
  • Loading branch information
JonathanPorta committed Jan 12, 2015
2 parents 48f4c32 + ac49a9f commit 70c729e
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 33 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -21,6 +21,7 @@ gem 'omniauth-facebook-access-token','0.1.6'
gem 'koala'
gem 'apns'
gem 'librato-rails'
gem 'bcrypt-ruby', require: 'bcrypt'

gem 'draper', '1.3.1'
gem 'verbs', '2.1.4'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Expand Up @@ -37,6 +37,9 @@ GEM
ast (2.0.0)
astrolabe (1.3.0)
parser (>= 2.2.0.pre.3, < 3.0)
bcrypt (3.1.9)
bcrypt-ruby (3.1.5)
bcrypt (>= 3.1.3)
better_errors (2.0.0)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
Expand Down Expand Up @@ -349,6 +352,7 @@ PLATFORMS
DEPENDENCIES
annotate
apns
bcrypt-ruby
better_errors
binding_of_caller
capybara
Expand Down
27 changes: 21 additions & 6 deletions app/controllers/application_controller.rb
@@ -1,20 +1,27 @@
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: :null_session
before_action :require_authentication
# protect_from_forgery with: :null_session

private

def api_token
logger.debug request.headers['HTTP_API_TOKEN']
request.headers['HTTP_API_TOKEN']
end

def api_version
request.headers['HTTP_API_VERSION']
end

def current_user
# logger.debug request.headers.inspect
logger.debug request.headers['HTTP_ACCESS_TOKEN']
# logger.debug request.headers['access_token']
if session[:user_id]
logger.warn 'Getting user because the session had a user_id.'
@current_user ||= User.find(session[:user_id]) if session[:user_id]
elsif request.headers['HTTP_ACCESS_TOKEN']
elsif api_token
logger.warn 'Getting user because request had an access token.'
@urrent_user ||= User.find_by_access_token request.headers['HTTP_ACCESS_TOKEN']
@current_user ||= User.authenticate_by_api_token api_token
end

rescue ActiveRecord::RecordNotFound => e
Expand All @@ -23,5 +30,13 @@ def current_user
redirect_to '/logout'
end

def require_authentication
unauthorized unless current_user
end

def unauthorized
render nothing: true, status: 401
end

helper_method :current_user
end
31 changes: 29 additions & 2 deletions app/controllers/sessions_controller.rb
@@ -1,16 +1,28 @@
class SessionsController < ApplicationController
skip_before_action :require_authentication, only: [:create, :login]

# GET /auth/:provider/callback
def create
logger.debug env['omniauth.auth']
user = User.from_omniauth env['omniauth.auth']
session[:user_id] = user.id
current_session user
redirect_to root_url
end

# POST /login
def login
user = User.authenticate login_params[:email], login_params[:password]
current_session user
redirect_to root_url
end

# GET /logout
def destroy
session[:user_id] = nil
current_session nil
redirect_to root_url
end

# GET /user
def show
@user = current_user
end
Expand All @@ -19,4 +31,19 @@ def failure
# TODO: Need to think about what should actually happen here.
redirect_to root_url
end

private

def current_session(user)
if user
session[:user_id] = user.id
else
session[:user_id] = nil
end
end

# Never trust parameters from the scary internet, only allow the white list through.
def login_params
params.permit(:email, :password)
end
end
17 changes: 17 additions & 0 deletions app/controllers/users_controller.rb
@@ -1,10 +1,27 @@
class UsersController < ApplicationController
before_action :set_user, only: []
skip_before_action :require_authentication, only: [:create]

# POST /users.json
def create
@user = User.new user_params

if @user.save
render :show, status: :created, location: @user
else
render json: @user.errors, status: :unprocessable_entity
end
end

private

# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end

# Never trust parameters from the scary internet, only allow the white list through.
def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password)
end
end
36 changes: 29 additions & 7 deletions app/models/user.rb
@@ -1,4 +1,6 @@
class User < ActiveRecord::Base
before_create :generate_api_token

has_many :devices
has_many :activities
has_many :messages
Expand All @@ -11,19 +13,17 @@ class User < ActiveRecord::Base

has_many :auth_providers

validates :email, :first_name, :last_name, presence: true
validates :id, absence: true, on: :create
validates :email, :first_name, :last_name, presence: true
validates :email, uniqueness: true
validates :api_token, uniqueness: true

has_secure_password validations: false

after_save do
Librato.measure 'users.count', User.count, sporadic: true
end

def self.find_by_access_token(token)
# TODO: Fix this when verb authprovider gets implemented
auth_provider = AuthProvider.where(provider: 'facebook', token: token).first
auth_provider.user if auth_provider
end

def self.from_omniauth(auth)
auth_provider = AuthProvider.from_omniauth auth

Expand All @@ -43,6 +43,19 @@ def self.from_omniauth(auth)
user
end

def self.authenticate(email, password)
User.find_by(email: email).try :authenticate, password
end

def self.authenticate_by_api_token(api_token)
User.find_by api_token: api_token
end

def self.authenticate_by_auth_provider(provider, token)
auth_provider = AuthProvider.where(provider: provider, token: token).first
auth_provider.user if auth_provider
end

def self.from_facebook(user_hash)
facebook_auth_provider = AuthProvider.where(provider: 'facebook', uid: user_hash['id']).first
facebook_auth_provider.user if facebook_auth_provider
Expand Down Expand Up @@ -72,4 +85,13 @@ def friendship_requests_sent
def friendship_requests_received
inverse_friendships.where approved: nil
end

private

def generate_api_token
self.api_token ||= loop do
random_token = SecureRandom.urlsafe_base64(15).tr('lIO0', 'sxyz')
break random_token unless self.class.exists?(api_token: random_token)
end
end
end
13 changes: 2 additions & 11 deletions app/views/sessions/show.json.jbuilder
@@ -1,11 +1,2 @@
if @user
json.(@user,
:id,
:first_name,
:last_name,
:birthday
)
else
json.error 'Not authed. Goto /auth/facebook'
json.todo 'TODO: raise a proper 401 :-)'
end
@user.decorate
json.extract! @user, :id, :email, :first_name, :last_name, :birthday
2 changes: 2 additions & 0 deletions app/views/users/show.json.jbuilder
@@ -0,0 +1,2 @@
user = @user.decorate
json.extract! user, :id, :email, :first_name, :last_name
6 changes: 4 additions & 2 deletions config/routes.rb
Expand Up @@ -19,12 +19,14 @@

# You can have the root of your site routed with "root"
root 'sessions#show', format: 'json'

get 'user' => 'sessions#show', format: 'json'
post 'login' => 'sessions#login', format: 'json'
match 'auth/:provider/callback', to: 'sessions#create', via: [:get, :post]
match 'auth/failure', to: redirect('/'), via: [:get, :post]
match 'logout', to: 'sessions#destroy', as: 'logout', via: [:get, :post]

get 'user' => 'sessions#show', format: 'json'
# Registration route when not using an auth_provider
post 'users' => 'users#create', format: 'json'

get 'messages', to: redirect('activities')
get 'messages/sent' => 'messages#sent'
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20150112014931_add_password_to_user.rb
@@ -0,0 +1,9 @@
class AddPasswordToUser < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
add_column :users, :api_token, :string

add_index :users, :email, unique: true
add_index :users, :api_token, unique: true
end
end
7 changes: 6 additions & 1 deletion db/schema.rb
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20141206021950) do
ActiveRecord::Schema.define(version: 20150112014931) do

# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
Expand Down Expand Up @@ -77,6 +77,11 @@
t.string "birthday"
t.datetime "created_at"
t.datetime "updated_at"
t.string "password_digest"
t.string "api_token"
end

add_index "users", ["api_token"], name: "index_users_on_api_token", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree

end
6 changes: 3 additions & 3 deletions spec/controllers/application_controller_spec.rb
Expand Up @@ -12,7 +12,7 @@ def index

before :each do
@user = FactoryGirl.create :user_with_facebook_auth
@valid_auth_token = @user.auth_providers.first.token
@valid_auth_token = @user.api_token
@invalid_auth_toke = 'INVALIDAUTHTOKEN'
end

Expand All @@ -27,13 +27,13 @@ def index
end

it 'returns correct user when valid header token is set' do
request.headers['HTTP_ACCESS_TOKEN'] = @valid_auth_token
request.headers['HTTP_API_TOKEN'] = @valid_auth_token
get :index, {}
expect(assigns(:current_user)).to eq(@user)
end

it 'returns nil when invalid header token is set' do
request.headers['HTTP_ACCESS_TOKEN'] = @invalid_auth_token
request.headers['HTTP_API_TOKEN'] = @invalid_auth_token
get :index, {}
expect(assigns(:current_user)).to eq(nil)
end
Expand Down
6 changes: 5 additions & 1 deletion spec/controllers/auth_providers_controller_spec.rb
Expand Up @@ -20,6 +20,10 @@

RSpec.describe AuthProvidersController, type: :controller do

before :each do
@user = FactoryGirl.create :user
end

# This should return the minimal set of attributes required to create a valid
# AuthProvider. As you add validations to AuthProvider, be sure to
# adjust the attributes here as well.
Expand All @@ -30,7 +34,7 @@
# This should return the minimal set of values that should be in the session
# in order to pass any filters (e.g. authentication) defined in
# AuthProvidersController. Be sure to keep this updated too.
let(:valid_session) { {} }
let(:valid_session) { { user_id: @user.id } }

describe 'GET index' do
it 'assigns all auth_providers as @auth_providers' do
Expand Down
1 change: 1 addition & 0 deletions spec/factories/users.rb
Expand Up @@ -6,6 +6,7 @@
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
birthday { Faker::Business.credit_card_expiry_date }
password 'password'

factory :user_with_facebook_auth do
after(:create) do |user|
Expand Down
13 changes: 13 additions & 0 deletions spec/models/user_spec.rb
Expand Up @@ -77,4 +77,17 @@
expect(@user.friends.first).to eq(@friend)
end
end

describe 'User authentication' do
before :each do
@user = FactoryGirl.create :user
@email = @user.email
@password = @user.password
end

it 'Should authenticate a user and return a model' do
user = User.authenticate @email, @password
expect(user).to eq(@user)
end
end
end
2 changes: 2 additions & 0 deletions spec/requests/auth_providers_spec.rb
Expand Up @@ -3,6 +3,8 @@
RSpec.describe 'AuthProviders', type: :request do
describe 'GET /auth_providers' do
it 'works! (now write some real specs)' do
login_with_oauth

get auth_providers_path
expect(response.status).to be(200)
end
Expand Down

0 comments on commit 70c729e

Please sign in to comment.