Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ gem 'hanami-validations', '0.6.0' # form validation
gem 'dry-validation', '0.10.4' # validation methods for reform
gem 'ability_list', '0.0.4'
gem 'activesupport', '5.0.0'
gem 'sidekiq', '4.2.9'

group :development, :test do
gem 'awesome_print', '1.7.0'
Expand Down
12 changes: 11 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
concurrent-ruby (1.0.4)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
daemons (1.2.4)
Expand Down Expand Up @@ -118,12 +119,15 @@ GEM
rack (>= 0.4)
rack-indifferent (1.1.0)
rack (>= 1.5)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
rake (11.2.2)
rb-fsevent (0.9.8)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
redis (3.3.3)
rerun (0.11.0)
listen (~> 3.0)
rspec (3.5.0)
Expand All @@ -142,6 +146,11 @@ GEM
ruby_dep (1.5.0)
safe_yaml (1.0.4)
sequel (4.40.0)
sidekiq (4.2.9)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
slop (3.6.0)
thin (1.7.0)
daemons (~> 1.0, >= 1.0.9)
Expand Down Expand Up @@ -190,6 +199,7 @@ DEPENDENCIES
rerun (= 0.11.0)
rspec (= 3.5.0)
sequel (= 4.40.0)
sidekiq (= 4.2.9)
thin (= 1.7.0)
uuidtools (= 2.1.5)
vcr (= 3.0.3)
Expand All @@ -199,4 +209,4 @@ RUBY VERSION
ruby 2.3.3p222

BUNDLED WITH
1.13.6
1.14.3
7 changes: 3 additions & 4 deletions application/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Api < Grape::API; end
require 'config/sequel'
require 'config/hanami'
require 'config/grape'
require 'config/sidekiq'

# require some global libs
require 'lib/core_ext'
Expand Down Expand Up @@ -54,11 +55,9 @@ class Api < Grape::API
helpers ApiResponse
include Auth

before do
authenticate!
end

Dir['./application/api_entities/**/*.rb'].each { |rb| require rb }
Dir['./application/validators/**/*.rb'].each { |rb| require rb }
Dir['./application/workers/**/*.rb'].each { |rb| require rb }
Dir['./application/api/**/*.rb'].each { |rb| require rb }

add_swagger_documentation \
Expand Down
87 changes: 87 additions & 0 deletions application/api/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,92 @@ class Api
data: users
}
end

params do
group :user, type: Hash do
requires(:email, type: String, desc: "Users email address")
requires(:last_name, type: String, desc: "Users last name")
requires(:first_name, type: String, desc: "Users first name")
requires(:password, type: String, desc: "Users password")
optional(:born_on, type: DateTime, desc: "Users birth date (YYYY-MM-DD)")
end
end

post do
user_validator = UserCreationValidator.new(params[:user])
validation_result = user_validator.validate
if validation_result.success?
user = Api::Models::User.create(params[:user])
WelcomeEmailWorker.perform_async(user.email)

present :user, user, with: API::Entities::User
else
error!({ errors: validation_result.messages }, 400)
end
end

params do
group :user, type: Hash do
optional(:email, type: String, desc: "Users email address")
optional(:last_name, type: String, desc: "Users last name")
optional(:first_name, type: String, desc: "Users first name")
optional(:born_on, type: DateTime, desc: "Users birth date (YYYY-MM-DD)")
end
end

desc "updates a user" do
headers XAuthToken: {
description: 'Valdates your identity',
required: true
}
end

put ':id' do
authenticate!
user = Api::Models::User.find(id: params[:id])
unauthorized! unless current_user.can? :edit, user

user_validator = UserUpdateValidator.new(params[:user])
validation_result = user_validator.validate

if validation_result.success?
user.update(params[:user])

present :user, user, with: API::Entities::User
else
error!({ errors: validation_result.messages }, 400)
end
end

params do
optional(:new_password, type: String, desc: "password to set")
optional(:new_password_confirmation, type: String, desc: "confirmation of the new password")
end

desc "Changes a user password" do
headers XAuthToken: {
description: 'Valdates your identity',
required: true
}
end

patch ':id/reset_password' do
authenticate!
user = Api::Models::User.find(id: params[:id])
unauthorized! unless current_user.can? :change_password, user

password_validator = ChangePasswordValidator.new(params.symbolize_keys)
validation_result = password_validator.validate

if validation_result.success?
user.update(password: params[:new_password])
PasswordChangedEmailWorker.perform_async(user.email)

present :success, true
else
error!({ errors: validation_result.messages }, 400)
end
end

end
end
10 changes: 10 additions & 0 deletions application/api_entities/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module API
module Entities
class User < Grape::Entity
expose :first_name
expose :last_name
expose :full_name
expose :born_on
end
end
end
10 changes: 9 additions & 1 deletion application/api_helpers/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ module Auth

module HelperMethods
def authenticate!
# Library to authenticate user can go here
token = env["X-Auth-Token"]
unauthorize! unless token.present?

@current_user = Api::Models::User.find(token: token)
unauthorized! unless @current_user.present?
end

def current_user
@current_user
end

def unauthorized!
error!({ error: "Unauthorized request" }, 401)
end
end
end
end
3 changes: 2 additions & 1 deletion application/config/hanami.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module FormPredicates
predicate(:datetime_str?) do |current|
# Format: YYYY-MM-DD HH:MM:SS TZ - ex: 2016-01-01 02:03:04 -0800
# Timezone is optional
current.match(/^\d{4}-\d{2}-\d{2} \d{1,2}\:\d{1,2}\:\d{1,2}( \-\d{4})?$/)
return true if current.kind_of? DateTime
current.to_s.match(/^\d{4}-\d{2}-\d{2} \d{1,2}\:\d{1,2}\:\d{1,2}( \-\d{4})?$/)
end
end
5 changes: 5 additions & 0 deletions application/config/mail.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'mail'

Mail.defaults do
delivery_method :smtp, address: "localhost", port: 1025
end
9 changes: 9 additions & 0 deletions application/config/sidekiq.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'sidekiq'

Sidekiq.configure_server do |config|
config.redis = { db: 1 }
end

Sidekiq.configure_client do |config|
config.redis = { db: 1 }
end
6 changes: 5 additions & 1 deletion application/lib/abilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def user_permissions(user)
can :edit, Models::User do |check_user|
next true if user.id == check_user.id
end

can :change_password, Models::User do |check_user|
next true if user.id == check_user.id
end
end
end
end
end
5 changes: 5 additions & 0 deletions application/migrate/1487335421_add_token_to_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Sequel.migration do
change do
add_column :users, :token, String
end
end
5 changes: 5 additions & 0 deletions application/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ module Models
class User < Sequel::Model(:users)
include AbilityList::Helpers

def before_create
self.token = SecureRandom.hex(18)
super
end

def abilities
@abilities ||= Abilities.new(self)
end
Expand Down
36 changes: 36 additions & 0 deletions application/spec/api/users/post_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'spec_helper'

describe 'POST /api/users' do
let(:user_attributes){ attributes_for(:user) }

it 'should create a new user' do
expect { post "api/v1.0/users", user: user_attributes }.to change{ Api::Models::User.count }.by(1)
end

it "should return the created user" do
post "api/v1.0/users", user: user_attributes
user = response_body[:user]
expect(user[:first_name]).to eq(user_attributes[:first_name])
expect(user[:last_name]).to eq(user_attributes[:last_name])
expect(user[:full_name]).to eq("#{user_attributes[:first_name]} #{user_attributes[:last_name]}")
end

it "should return validation errors" do
user_attributes[:email] = "juanchopolo@"
post "api/v1.0/users", user: user_attributes

errors = response_body[:errors]
expect(errors).not_to be_empty
expect(errors[:email].first).to eq "must be an email"
end

it "should add a job to send welcome email" do
expect{ post "api/v1.0/users", user: user_attributes }.to change{ WelcomeEmailWorker.jobs.size }.by(1)
end

it "should queue a job to send an email to the user's email" do
post "api/v1.0/users", user: user_attributes

expect(WelcomeEmailWorker.jobs.last["args"]).to eq [user_attributes[:email]]
end
end
34 changes: 34 additions & 0 deletions application/spec/api/users/put_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'spec_helper'

describe 'PUT /api/users/:id' do
before do
@user = create(:user, email: 'test_user@test.com', password: 'password')
end

it "should update a user" do
put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => @user.token }
expect(response_body[:user][:full_name]).to eq "Lionel Messi"
expect(Api::Models::User.find(id: @user.id).full_name).to eq "Lionel Messi"
end

it "should return validation errors" do
put "api/v1.0/users/#{@user.id}", { user: {email: "wrong_email"} }, {'X-Auth-Token' => @user.token }

errors = response_body[:errors]
expect(errors).not_to be_empty
expect(errors[:email].first).to eq "must be an email"
end

it "should reject a request with the wrong token" do
put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => '123345677' }
expect(last_response.status).to eq 401
expect(response_body[:error]).to eq "Unauthorized request"
end

it "should reject a request with another user's token" do
another_user = create(:user)
put "api/v1.0/users/#{@user.id}", { user: {first_name: "Lionel", last_name: "Messi"} }, {'X-Auth-Token' => another_user.token }
expect(last_response.status).to eq 401
expect(response_body[:error]).to eq "Unauthorized request"
end
end
43 changes: 43 additions & 0 deletions application/spec/api/users/reset_password_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'spec_helper'

describe 'PATCH /api/users/:id/reset_password' do
before do
@user = create(:user, password: '123456')
end

it "should update a user" do
patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token }
expect(response_body[:success]).to eq true
expect(Api::Models::User.find(id: @user.id).password).to eq "password"
end

it "should return validation errors" do
patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'different' }, {'X-Auth-Token' => @user.token }

errors = response_body[:errors]
expect(errors).not_to be_empty
expect(errors[:new_password_confirmation].first).to eq "must be equal to password"
end

it "should reject a request with the wrong token" do
patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => '123345677' }
expect(response_body[:error]).to eq "Unauthorized request"
end

it "should reject a request with another user's token" do
another_user = create(:user)
patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => another_user.token }
expect(last_response.status).to eq 401
expect(response_body[:error]).to eq "Unauthorized request"
end

it "should add a job to send welcome email" do
expect{ patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token } }.to change{ PasswordChangedEmailWorker.jobs.size }.by(1)
end

it "should queue a job to send an email to the user's email" do
patch "api/v1.0/users/#{@user.id}/reset_password", { new_password: 'password', new_password_confirmation: 'password' }, {'X-Auth-Token' => @user.token }

expect(PasswordChangedEmailWorker.jobs.last["args"]).to eq [@user.email]
end
end
Loading