From e50794c64bf47db9dd1ebe8557d8f768111c5181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 12:40:00 -0500 Subject: [PATCH 01/18] Rename User's Columns * password -> encrypted_password * born_on -> date_of_birth --- application/migrate/1488562598_rename_user_columns.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 application/migrate/1488562598_rename_user_columns.rb diff --git a/application/migrate/1488562598_rename_user_columns.rb b/application/migrate/1488562598_rename_user_columns.rb new file mode 100644 index 0000000..a49e50e --- /dev/null +++ b/application/migrate/1488562598_rename_user_columns.rb @@ -0,0 +1,6 @@ +Sequel.migration do + change do + rename_column :users, :password, :encrypted_password + rename_column :users, :born_on, :date_of_birth + end +end From 91a85ad92eeb03b0334bf1b9bd6700ac84db11c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 13:10:27 -0500 Subject: [PATCH 02/18] Use Bcrypt to handle user's password --- Gemfile | 1 + Gemfile.lock | 4 +++- application/models/user.rb | 14 ++++++++++++++ application/spec/api/models/user_spec.rb | 22 ++++++++++++++++++++++ application/spec/factories/user.rb | 4 ++-- 5 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 application/spec/api/models/user_spec.rb diff --git a/Gemfile b/Gemfile index b654c5a..c185a8d 100644 --- a/Gemfile +++ b/Gemfile @@ -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 'bcrypt', '3.1.11' # encryption group :development, :test do gem 'awesome_print', '1.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index ad129a5..3e2eb8e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,7 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.11) builder (3.2.2) coderay (1.1.1) coercible (1.0.0) @@ -170,6 +171,7 @@ DEPENDENCIES ability_list (= 0.0.4) activesupport (= 5.0.0) awesome_print (= 1.7.0) + bcrypt (= 3.1.11) database_cleaner (= 1.5.3) dry-validation (= 0.10.4) factory_girl (= 4.7.0) @@ -199,4 +201,4 @@ RUBY VERSION ruby 2.3.3p222 BUNDLED WITH - 1.13.6 + 1.14.5 diff --git a/application/models/user.rb b/application/models/user.rb index 802a14b..dc892c3 100644 --- a/application/models/user.rb +++ b/application/models/user.rb @@ -1,3 +1,4 @@ +require 'bcrypt' require 'lib/abilities' class Api @@ -12,6 +13,19 @@ def abilities def full_name "#{self.first_name} #{self.last_name}" end + + def password + return if self.encrypted_password.nil? + + @password ||= BCrypt::Password.new(self.encrypted_password) + end + + def password=(new_password) + return if new_password.nil? + + @password = BCrypt::Password.create(new_password) + self.encrypted_password = @password + end end end end diff --git a/application/spec/api/models/user_spec.rb b/application/spec/api/models/user_spec.rb new file mode 100644 index 0000000..7697b77 --- /dev/null +++ b/application/spec/api/models/user_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Api::Models::User do + + describe "#password" do + + let(:user) { create(:user, password: 'test123') } + + it "should set encrypted_password" do + expect(user.encrypted_password).to be_present + expect(user.encrypted_password).not_to eq('test123') + end + + it "should delegate comparison to BCrypt library" do + expect(user.password.class).to eq(BCrypt::Password) + expect(user.password).to eq('test123') + end + + end + + +end diff --git a/application/spec/factories/user.rb b/application/spec/factories/user.rb index 199c292..7fad825 100644 --- a/application/spec/factories/user.rb +++ b/application/spec/factories/user.rb @@ -3,7 +3,7 @@ first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } email { Faker::Internet.email } - password Digest::MD5.hexdigest 'test' - born_on Date.new(2000, 1, 1) + password 'test' + date_of_birth Date.new(2000, 1, 1) end end From 7339030115819ee41ce4dba626b631f30e64aa6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 14:47:03 -0500 Subject: [PATCH 03/18] Add authentication method. --- .env.development.sample | 1 + .env.test.sample | 1 + Gemfile | 1 + Gemfile.lock | 2 ++ application/api.rb | 4 ++++ application/api_helpers/auth.rb | 10 +++++++++- application/config/variables.rb | 1 + application/models/user.rb | 1 - 8 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.env.development.sample b/.env.development.sample index 7864698..ae2d78d 100644 --- a/.env.development.sample +++ b/.env.development.sample @@ -3,3 +3,4 @@ DATABASE_URL=mysql2://root:@127.0.0.1:3306/sample?reconnect=true MAIL_URL=smtp://127.0.0.1:1025 SYSTEM_EMAIL=support@sample.com SITE_URL=http://localhost:3000/ +HMAC_SECRET=ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY diff --git a/.env.test.sample b/.env.test.sample index 67326c8..8a3c122 100644 --- a/.env.test.sample +++ b/.env.test.sample @@ -3,3 +3,4 @@ DATABASE_URL=mysql2://root:@127.0.0.1:3306/sample_test?reconnect=true MAIL_URL=smtp://127.0.0.1:1025 SYSTEM_EMAIL=support@sample.com SITE_URL=http://localhost:3000/ +HMAC_SECRET=ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY diff --git a/Gemfile b/Gemfile index c185a8d..78515d1 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,7 @@ gem 'dry-validation', '0.10.4' # validation methods for reform gem 'ability_list', '0.0.4' gem 'activesupport', '5.0.0' gem 'bcrypt', '3.1.11' # encryption +gem 'jwt', '1.5.6' # Json Web Token group :development, :test do gem 'awesome_print', '1.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3e2eb8e..f311921 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,6 +91,7 @@ GEM i18n (0.7.0) ice_nine (0.11.2) inflecto (0.0.2) + jwt (1.5.6) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -182,6 +183,7 @@ DEPENDENCIES grape-swagger (= 0.25.1) grape-swagger-entity (= 0.1.5) hanami-validations (= 0.6.0) + jwt (= 1.5.6) mail (= 2.6.4) mysql2 (= 0.4.5) pry (= 0.10.4) diff --git a/application/api.rb b/application/api.rb index 7ddc2d9..aa5b671 100644 --- a/application/api.rb +++ b/application/api.rb @@ -32,6 +32,10 @@ class Api < Grape::API; end require 'active_support' require 'active_support/core_ext' +# require authentication libs +require 'jwt' +require 'bcrypt' + # require all models Dir['./application/models/*.rb'].each { |rb| require rb } diff --git a/application/api_helpers/auth.rb b/application/api_helpers/auth.rb index b46a936..95dba17 100644 --- a/application/api_helpers/auth.rb +++ b/application/api_helpers/auth.rb @@ -8,7 +8,15 @@ module Auth module HelperMethods def authenticate! - # Library to authenticate user can go here + token = request.env['HTTP_AUTHORIZATION'] + payload = JWT.decode(token, HMAC_SECRET)[0] + if @current_user = ::Api::Models::User.where(email: payload['email']).first + true + else + error!('Unauthorized', 401) + end + rescue JWT::DecodeError, JWT::VerificationError + error!('Unauthorized', 401) end def current_user diff --git a/application/config/variables.rb b/application/config/variables.rb index 4916a70..cec202e 100644 --- a/application/config/variables.rb +++ b/application/config/variables.rb @@ -23,3 +23,4 @@ MAIL_URL = ENV.fetch('MAIL_URL').freeze SYSTEM_EMAIL = ENV.fetch('SYSTEM_EMAIL').freeze SITE_URL = ENV.fetch('SITE_URL').freeze +HMAC_SECRET = ENV.fetch('HMAC_SECRET').freeze diff --git a/application/models/user.rb b/application/models/user.rb index dc892c3..2973bbc 100644 --- a/application/models/user.rb +++ b/application/models/user.rb @@ -1,4 +1,3 @@ -require 'bcrypt' require 'lib/abilities' class Api From 72b651dc590e1a42cfcad38b8003f501d91a109d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 15:01:03 -0500 Subject: [PATCH 04/18] Add Api::Entities::User to handle api response --- application/api.rb | 5 +---- application/api/users.rb | 5 ++--- application/api_entities/user.rb | 13 +++++++++++++ application/api_helpers/api_format.rb | 10 ++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 application/api_entities/user.rb create mode 100644 application/api_helpers/api_format.rb diff --git a/application/api.rb b/application/api.rb index aa5b671..f6e0f1c 100644 --- a/application/api.rb +++ b/application/api.rb @@ -56,12 +56,9 @@ class Api < Grape::API helpers SharedParams helpers ApiResponse + helpers ApiFormat include Auth - before do - authenticate! - end - Dir['./application/api_entities/**/*.rb'].each { |rb| require rb } Dir['./application/api/**/*.rb'].each { |rb| require rb } diff --git a/application/api/users.rb b/application/api/users.rb index 40de636..d46aee7 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -5,9 +5,8 @@ class Api end get do users = SEQUEL_DB[:users].all - { - data: users - } + + present users, with: Api::Entities::User end end end diff --git a/application/api_entities/user.rb b/application/api_entities/user.rb new file mode 100644 index 0000000..34a746b --- /dev/null +++ b/application/api_entities/user.rb @@ -0,0 +1,13 @@ +class Api + module Entities + class User < Grape::Entity + + expose :full_name + expose :first_name + expose :last_name + expose :email + expose :date_of_birth, format_with: :datetime_string + + end + end +end diff --git a/application/api_helpers/api_format.rb b/application/api_helpers/api_format.rb new file mode 100644 index 0000000..c0983f8 --- /dev/null +++ b/application/api_helpers/api_format.rb @@ -0,0 +1,10 @@ +class Api + module ApiFormat + extend Grape::API::Helpers + + Grape::Entity.format_with :datetime_string do |date| + date.to_time.to_s if date + end + + end +end From a485d8405bea190d0e3947c1c389fc293791f9f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 16:24:02 -0500 Subject: [PATCH 05/18] Endpoint for creating users. --- application/api.rb | 4 ++++ application/api/users.rb | 14 +++++++++++++- application/api_entities/user.rb | 2 ++ application/api_validators/create_user.rb | 16 ++++++++++++++++ application/spec/api/users/get_spec.rb | 2 +- application/spec/api/users/post_spec.rb | 17 +++++++++++++++++ application/spec/factories/user.rb | 2 +- 7 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 application/api_validators/create_user.rb create mode 100644 application/spec/api/users/post_spec.rb diff --git a/application/api.rb b/application/api.rb index f6e0f1c..0f91f16 100644 --- a/application/api.rb +++ b/application/api.rb @@ -27,6 +27,7 @@ class Api < Grape::API; end require 'lib/core_ext' require 'lib/time_formats' require 'lib/io' +require 'lib/pretty_logger' # load active support helpers require 'active_support' @@ -39,6 +40,7 @@ class Api < Grape::API; end # require all models Dir['./application/models/*.rb'].each { |rb| require rb } +Dir['./application/api_validators/**/*.rb'].each { |rb| require rb } Dir['./application/api_helpers/**/*.rb'].each { |rb| require rb } class Api < Grape::API version 'v1.0', using: :path @@ -54,6 +56,8 @@ class Api < Grape::API error! ret, 400 end + logger PrettyLogger.new + helpers SharedParams helpers ApiResponse helpers ApiFormat diff --git a/application/api/users.rb b/application/api/users.rb index d46aee7..5e694d4 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -1,12 +1,24 @@ class Api resource :users do + params do includes :basic_search end + get do users = SEQUEL_DB[:users].all - present users, with: Api::Entities::User end + + post do + result = Api::Validators::CreateUser.new(params).validate + error!({ messages: result.messages } , 422) unless result.success? + + user = Api::Models::User.new(result.output) + user.save + + present user, with: Api::Entities::User + end + end end diff --git a/application/api_entities/user.rb b/application/api_entities/user.rb index 34a746b..07a570d 100644 --- a/application/api_entities/user.rb +++ b/application/api_entities/user.rb @@ -1,7 +1,9 @@ class Api module Entities class User < Grape::Entity + root 'users', 'user' + expose :id expose :full_name expose :first_name expose :last_name diff --git a/application/api_validators/create_user.rb b/application/api_validators/create_user.rb new file mode 100644 index 0000000..d51697e --- /dev/null +++ b/application/api_validators/create_user.rb @@ -0,0 +1,16 @@ +class Api + module Validators + class CreateUser + include Hanami::Validations + predicates FormPredicates + + validations do + required('first_name') { filled? & str? } + required('last_name') { filled? & str? } + required('email') { email? } + required('password') { filled? & str? } + optional('date_of_birth') { datetime_str? } + end + end + end +end diff --git a/application/spec/api/users/get_spec.rb b/application/spec/api/users/get_spec.rb index 95899aa..56edaa4 100644 --- a/application/spec/api/users/get_spec.rb +++ b/application/spec/api/users/get_spec.rb @@ -9,7 +9,7 @@ it 'should pull all users' do get "api/v1.0/users" body = response_body - emails = body[:data].map{ |x| x[:email] } + emails = body[:users].map{ |x| x[:email] } expect(emails).to include @u1.email expect(emails).to include @u2.email end diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb new file mode 100644 index 0000000..935a542 --- /dev/null +++ b/application/spec/api/users/post_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'POST /api/users' do + + context "with valid params" do + + let(:user_params) { FactoryGirl.attributes_for(:user) } + + it 'should create the user' do + post "api/v1.0/users", user_params + user = response_body[:user] + expect(user[:id]).to be_present + expect(user[:email]).to eq(user_params[:email]) + end + + end +end diff --git a/application/spec/factories/user.rb b/application/spec/factories/user.rb index 7fad825..b02e6aa 100644 --- a/application/spec/factories/user.rb +++ b/application/spec/factories/user.rb @@ -4,6 +4,6 @@ last_name { Faker::Name.last_name } email { Faker::Internet.email } password 'test' - date_of_birth Date.new(2000, 1, 1) + date_of_birth Date.new(2000, 1, 1).to_time.to_s end end From 1d769a3934846e7f3d0d31e37e5b8b81e1997aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 16:46:23 -0500 Subject: [PATCH 06/18] Specify params in grape. --- application/api/users.rb | 12 ++++++++++-- application/spec/api/users/post_spec.rb | 25 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index 5e694d4..e89a7ea 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -10,9 +10,17 @@ class Api present users, with: Api::Entities::User end + params do + requires :first_name, type: String + requires :last_name, type: String + requires :email, type: String + requires :password, type: String + optional :date_of_birth, type: String + end + post do - result = Api::Validators::CreateUser.new(params).validate - error!({ messages: result.messages } , 422) unless result.success? + result = Api::Validators::CreateUser.new(declared(params)).validate + error!({ errors: result.messages } , 422) unless result.success? user = Api::Models::User.new(result.output) user.save diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb index 935a542..28c2963 100644 --- a/application/spec/api/users/post_spec.rb +++ b/application/spec/api/users/post_spec.rb @@ -6,6 +6,12 @@ let(:user_params) { FactoryGirl.attributes_for(:user) } + it do + expect { + post "api/v1.0/users", user_params + }.to change{ Api::Models::User.count }.by(1) + end + it 'should create the user' do post "api/v1.0/users", user_params user = response_body[:user] @@ -14,4 +20,23 @@ end end + + context "with invalid params" do + + let(:user_params) { { } } + + it do + expect { + post "api/v1.0/users", user_params + }.not_to change{ Api::Models::User.count } + end + + it 'should not create the user' do + post "api/v1.0/users", user_params + errors = response_body[:errors] + expect(errors).to be_present + expect(errors[:first_name]).to be_present + end + + end end From 2c29a1bb8000d45bd3da29818451f1973dbd8e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 17:51:39 -0500 Subject: [PATCH 07/18] Endpoint for updating users. --- application/api/users.rb | 38 ++++++++++++++++++ .../api_validators/reset_user_password.rb | 19 +++++++++ application/api_validators/update_user.rb | 15 +++++++ application/config/yaml/errors.yml | 1 + application/spec/api/users/put_spec.rb | 23 +++++++++++ .../spec/api/users/reset_password_spec.rb | 39 +++++++++++++++++++ 6 files changed, 135 insertions(+) create mode 100644 application/api_validators/reset_user_password.rb create mode 100644 application/api_validators/update_user.rb create mode 100644 application/spec/api/users/put_spec.rb create mode 100644 application/spec/api/users/reset_password_spec.rb diff --git a/application/api/users.rb b/application/api/users.rb index e89a7ea..16f1fc9 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -28,5 +28,43 @@ class Api present user, with: Api::Entities::User end + params do + optional :first_name, type: String + optional :last_name, type: String + optional :email, type: String + optional :date_of_birth, type: String + end + + put '/:id' do + result = Api::Validators::UpdateUser.new(declared(params)).validate + error!({ errors: result.messages } , 422) unless result.success? + + if user = Api::Models::User.where(id: params[:id]).first + user.update(result.output) + + present user, with: Api::Entities::User + else + api_response({error_type: :not_found}) + end + end + + params do + requires :new_password, type: String + requires :confirm_password, type: String + end + + patch '/:id/reset_password' do + result = Api::Validators::ResetUserPassword.new(declared(params)).validate + error!({ errors: result.messages } , 422) unless result.success? + + if user = Api::Models::User.where(id: params[:id]).first + user.password = result.output['new_password'] + user.save + api_response({status: :ok}) + else + api_response({error_type: :not_found}) + end + end + end end diff --git a/application/api_validators/reset_user_password.rb b/application/api_validators/reset_user_password.rb new file mode 100644 index 0000000..2266ff8 --- /dev/null +++ b/application/api_validators/reset_user_password.rb @@ -0,0 +1,19 @@ +class Api + module Validators + class ResetUserPassword + + include Hanami::Validations + predicates FormPredicates + + validations do + required('new_password').filled(:str?) + required('confirm_password').filled(:str?) + + rule(password_doesnt_match: ['new_password', 'confirm_password']) do |new_password, confirm_password| + new_password.eql?(confirm_password) + end + end + + end + end +end diff --git a/application/api_validators/update_user.rb b/application/api_validators/update_user.rb new file mode 100644 index 0000000..e988719 --- /dev/null +++ b/application/api_validators/update_user.rb @@ -0,0 +1,15 @@ +class Api + module Validators + class UpdateUser + include Hanami::Validations + predicates FormPredicates + + validations do + optional('first_name') { filled? & str? } + optional('last_name') { filled? & str? } + optional('email') { email? } + optional('date_of_birth') { datetime_str? } + end + end + end +end diff --git a/application/config/yaml/errors.yml b/application/config/yaml/errors.yml index 4128a88..b771915 100644 --- a/application/config/yaml/errors.yml +++ b/application/config/yaml/errors.yml @@ -3,3 +3,4 @@ en: email?: must be an email datetime_str?: must be in format YYYY-MM-DD HH:MM:SS phone?: must be valid format (ex - 8005556262) + password_doesnt_match: new_password and confirm_password doesn't match diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb new file mode 100644 index 0000000..e71f4e1 --- /dev/null +++ b/application/spec/api/users/put_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'PUT /api/users/:id' do + + context "with valid params" do + + let(:user) { create(:user) } + let(:user_id) { user.id } + let(:params) { FactoryGirl.attributes_for(:user, except: [:password]) } + + it 'should create the user' do + put "api/v1.0/users/#{user_id}", params + user = response_body[:user] + expect(user[:id]).to eq(user_id) + expect(user[:first_name]).to eq(params[:first_name]) + expect(user[:last_name]).to eq(params[:last_name]) + expect(user[:email]).to eq(params[:email]) + expect(user[:date_of_birth]).to eq(params[:date_of_birth]) + end + + end + +end diff --git a/application/spec/api/users/reset_password_spec.rb b/application/spec/api/users/reset_password_spec.rb new file mode 100644 index 0000000..53f95b2 --- /dev/null +++ b/application/spec/api/users/reset_password_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'PATCH /api/users/:id/reset_password' do + + let!(:user) { create(:user, password: 'oldpass') } + let(:user_id) { user.id } + + context "with valid params" do + + let(:params) { { new_password: 'new_pass123', confirm_password: 'new_pass123'} } + + it "should reset password" do + patch "api/v1.0/users/#{user_id}/reset_password", params + + user = Api::Models::User.where(id: user_id).first + + expect(response_body[:errors]).not_to be_present + expect(user.reload.password).not_to eq('oldpass') + expect(user.password).to eq(params[:new_password]) + end + + end + + context "with invalid params" do + + let(:params) { { new_password: 'notmatch', confirm_password: 'new_pass123'} } + + it "should reset password" do + patch "api/v1.0/users/#{user.id}/reset_password", params + errors = response_body[:errors] + expect(errors).to be_present + expect(errors[:password_doesnt_match]).to be_present + expect(user.password).to eq('oldpass') + end + + end + + +end From 531071fc1313f1c7a810fffed05a2b2aed4c5677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 18:09:03 -0500 Subject: [PATCH 08/18] Authenticate endpoint PUT /api/users/:id --- application/api/users.rb | 2 ++ application/api_helpers/auth.rb | 4 ++-- application/spec/api/users/put_spec.rb | 24 +++++++++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index 16f1fc9..7a4f16c 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -36,6 +36,8 @@ class Api end put '/:id' do + authenticate! + result = Api::Validators::UpdateUser.new(declared(params)).validate error!({ errors: result.messages } , 422) unless result.success? diff --git a/application/api_helpers/auth.rb b/application/api_helpers/auth.rb index 95dba17..0151e9d 100644 --- a/application/api_helpers/auth.rb +++ b/application/api_helpers/auth.rb @@ -13,10 +13,10 @@ def authenticate! if @current_user = ::Api::Models::User.where(email: payload['email']).first true else - error!('Unauthorized', 401) + error!({error_type: 'not_authorized'}, 401) end rescue JWT::DecodeError, JWT::VerificationError - error!('Unauthorized', 401) + error!({error_type: 'not_authorized'}, 401) end def current_user diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb index e71f4e1..5a0b15c 100644 --- a/application/spec/api/users/put_spec.rb +++ b/application/spec/api/users/put_spec.rb @@ -1,15 +1,29 @@ require 'spec_helper' -describe 'PUT /api/users/:id' do +describe 'PUT /api/users/:id', :type => :request do + + let(:user) { create(:user) } + let(:user_id) { user.id } + let(:params) { FactoryGirl.attributes_for(:user, except: [:password]) } + + context "Protected endpoint" do + it 'doesnt allow unauthorized requests' do + put "api/v1.0/users/#{user_id}", params + expect(response_body[:error_type]).to eq('not_authorized') + end + end context "with valid params" do - let(:user) { create(:user) } - let(:user_id) { user.id } - let(:params) { FactoryGirl.attributes_for(:user, except: [:password]) } + let(:token) do + payload = { email: user.email } + JWT.encode(payload, HMAC_SECRET) + end + + let(:headers) { { 'HTTP_AUTHORIZATION' => token} } it 'should create the user' do - put "api/v1.0/users/#{user_id}", params + put "api/v1.0/users/#{user_id}", params, headers user = response_body[:user] expect(user[:id]).to eq(user_id) expect(user[:first_name]).to eq(params[:first_name]) From 625b2396a05d184f93a53d1ed6e8fb8accbedd5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 18:29:50 -0500 Subject: [PATCH 09/18] Use current user to update their own record. --- application/api/users.rb | 9 ++------- application/spec/spec_helper.rb | 1 + 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index 7a4f16c..0127b08 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -41,13 +41,8 @@ class Api result = Api::Validators::UpdateUser.new(declared(params)).validate error!({ errors: result.messages } , 422) unless result.success? - if user = Api::Models::User.where(id: params[:id]).first - user.update(result.output) - - present user, with: Api::Entities::User - else - api_response({error_type: :not_found}) - end + current_user.update(result.output) + present current_user, with: Api::Entities::User end params do diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index 4b54421..8616f1d 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -49,6 +49,7 @@ def current_user rescue nil end + super end end end From 5ebbd9c5c8da5b3092f248346366bd56a2057cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Fri, 3 Mar 2017 18:47:05 -0500 Subject: [PATCH 10/18] Use ability in PUT /users. --- application/api/users.rb | 13 +++++++++++-- application/spec/api/users/put_spec.rb | 21 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index 0127b08..e244124 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -41,8 +41,17 @@ class Api result = Api::Validators::UpdateUser.new(declared(params)).validate error!({ errors: result.messages } , 422) unless result.success? - current_user.update(result.output) - present current_user, with: Api::Entities::User + unless user = Api::Models::User.where(id: params[:id]).first + return api_response({error_type: :not_found}) + end + + unless current_user.can?(:edit, user) + return api_response({error_type: :forbidden}) + end + + user.update(result.output) + + present user, with: Api::Entities::User end params do diff --git a/application/spec/api/users/put_spec.rb b/application/spec/api/users/put_spec.rb index 5a0b15c..438b6d4 100644 --- a/application/spec/api/users/put_spec.rb +++ b/application/spec/api/users/put_spec.rb @@ -22,7 +22,7 @@ let(:headers) { { 'HTTP_AUTHORIZATION' => token} } - it 'should create the user' do + it 'should update the user' do put "api/v1.0/users/#{user_id}", params, headers user = response_body[:user] expect(user[:id]).to eq(user_id) @@ -34,4 +34,23 @@ end + context "update another user" do + + let(:another_user) { create(:user) } + let(:user_id) { another_user.id } + + let(:token) do + payload = { email: user.email } + JWT.encode(payload, HMAC_SECRET) + end + + let(:headers) { { 'HTTP_AUTHORIZATION' => token} } + + it 'should NOT update another user' do + put "api/v1.0/users/#{user_id}", params, headers + expect(response_body[:error_type]).to eq('forbidden') + end + + end + end From e3fd501ca2f80b10e0d8a3d0087e821b21788d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 09:42:31 -0500 Subject: [PATCH 11/18] Add enpoint POST /api/users/signin --- application/api/users.rb | 21 ++++++++ application/api_helpers/api_response.rb | 2 + application/api_helpers/auth.rb | 5 ++ application/api_validators/user_signin.rb | 13 +++++ .../spec/api/users/post_signin_spec.rb | 50 +++++++++++++++++++ application/spec/spec_helper.rb | 4 ++ 6 files changed, 95 insertions(+) create mode 100644 application/api_validators/user_signin.rb create mode 100644 application/spec/api/users/post_signin_spec.rb diff --git a/application/api/users.rb b/application/api/users.rb index e244124..c0c77e7 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -72,5 +72,26 @@ class Api end end + params do + requires :email, type: String + requires :password, type: String + end + + post '/signin' do + result = Api::Validators::UserSignin.new(declared(params)).validate + error!({ errors: result.messages } , 422) unless result.success? + + unless user = Api::Models::User.where(email: result.output['email']).first + return api_response({error_type: :unauthorized}) + end + + unless user.password == result.output['password'] + return api_response({error_type: :unauthorized}) + end + + token = generate_token_for_user(user) + api_response({token: token}) + end + end end diff --git a/application/api_helpers/api_response.rb b/application/api_helpers/api_response.rb index 4613eca..fd87a43 100644 --- a/application/api_helpers/api_response.rb +++ b/application/api_helpers/api_response.rb @@ -9,6 +9,8 @@ def api_response response status 404 when :forbidden status 403 + when :unauthorized + status 401 else status 400 end diff --git a/application/api_helpers/auth.rb b/application/api_helpers/auth.rb index 0151e9d..dd2553b 100644 --- a/application/api_helpers/auth.rb +++ b/application/api_helpers/auth.rb @@ -22,6 +22,11 @@ def authenticate! def current_user @current_user end + + def generate_token_for_user(user) + payload = { email: user.email } + JWT.encode(payload, HMAC_SECRET) + end end end end diff --git a/application/api_validators/user_signin.rb b/application/api_validators/user_signin.rb new file mode 100644 index 0000000..28e5c09 --- /dev/null +++ b/application/api_validators/user_signin.rb @@ -0,0 +1,13 @@ +class Api + module Validators + class UserSignin + include Hanami::Validations + predicates FormPredicates + + validations do + required('email') { email? } + required('password') { filled? & str? } + end + end + end +end diff --git a/application/spec/api/users/post_signin_spec.rb b/application/spec/api/users/post_signin_spec.rb new file mode 100644 index 0000000..f243154 --- /dev/null +++ b/application/spec/api/users/post_signin_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe 'POST /api/users/signin' do + + let(:user) { create(:user, password: 'mypass123') } + + context "with valid params" do + + let(:credentials) { { email: user.email, password: 'mypass123' } } + + it "should deliver user's token" do + post "api/v1.0/users/signin", credentials + expect(response_status).to eq(200) + expect(response_body[:token]).to be_present + end + + end + + context "with invalid params" do + + let(:user_email) { user.email } + let(:user_password) { 'mypass123' } + let(:bad_credentials) { { email:user_email, password: user_password } } + + context "wrong password" do + + let(:user_password) { 'anotherpass' } + + it 'should return unauthorized request' do + post "api/v1.0/users/signin", bad_credentials + expect(response_status).to eq(401) + expect(response_body[:error_type]).to eq('unauthorized') + end + + end + + context "wrong email" do + + let(:user_email) { 'another@email.com' } + + it 'should return unauthorized request' do + post "api/v1.0/users/signin", bad_credentials + expect(response_status).to eq(401) + expect(response_body[:error_type]).to eq('unauthorized') + end + + end + + end +end diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index 8616f1d..ca657ec 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -34,6 +34,10 @@ def response_body JSON.parse(last_response.body, symbolize_names: true) end + def response_status + last_response.status + end + def get_scope opts = {} scope = Api.new scope.instance_variable_set(:@current_user, opts[:as_user]) From ba40930fe83d1135411be34a54d8afd109ae22a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 09:49:20 -0500 Subject: [PATCH 12/18] Improve error messages for reset password endpoint --- application/api/users.rb | 13 +++++++------ application/api_validators/reset_user_password.rb | 2 +- application/config/yaml/errors.yml | 2 +- application/spec/api/users/reset_password_spec.rb | 7 +++++-- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index c0c77e7..1844dda 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -63,13 +63,14 @@ class Api result = Api::Validators::ResetUserPassword.new(declared(params)).validate error!({ errors: result.messages } , 422) unless result.success? - if user = Api::Models::User.where(id: params[:id]).first - user.password = result.output['new_password'] - user.save - api_response({status: :ok}) - else - api_response({error_type: :not_found}) + unless user = Api::Models::User.where(id: params[:id]).first + return api_response({error_type: :not_found}) end + + user.password = result.output['new_password'] + user.save + + api_response({status: :ok}) end params do diff --git a/application/api_validators/reset_user_password.rb b/application/api_validators/reset_user_password.rb index 2266ff8..eb38c48 100644 --- a/application/api_validators/reset_user_password.rb +++ b/application/api_validators/reset_user_password.rb @@ -9,7 +9,7 @@ class ResetUserPassword required('new_password').filled(:str?) required('confirm_password').filled(:str?) - rule(password_doesnt_match: ['new_password', 'confirm_password']) do |new_password, confirm_password| + rule(confirm_password: ['new_password', 'confirm_password']) do |new_password, confirm_password| new_password.eql?(confirm_password) end end diff --git a/application/config/yaml/errors.yml b/application/config/yaml/errors.yml index b771915..92ea01e 100644 --- a/application/config/yaml/errors.yml +++ b/application/config/yaml/errors.yml @@ -3,4 +3,4 @@ en: email?: must be an email datetime_str?: must be in format YYYY-MM-DD HH:MM:SS phone?: must be valid format (ex - 8005556262) - password_doesnt_match: new_password and confirm_password doesn't match + confirm_password: doesn't match diff --git a/application/spec/api/users/reset_password_spec.rb b/application/spec/api/users/reset_password_spec.rb index 53f95b2..8fe06f4 100644 --- a/application/spec/api/users/reset_password_spec.rb +++ b/application/spec/api/users/reset_password_spec.rb @@ -14,6 +14,7 @@ user = Api::Models::User.where(id: user_id).first + expect(response_status).to eq(200) expect(response_body[:errors]).not_to be_present expect(user.reload.password).not_to eq('oldpass') expect(user.password).to eq(params[:new_password]) @@ -25,11 +26,13 @@ let(:params) { { new_password: 'notmatch', confirm_password: 'new_pass123'} } - it "should reset password" do + it "should return an error" do patch "api/v1.0/users/#{user.id}/reset_password", params + errors = response_body[:errors] + expect(response_status).to eq(422) expect(errors).to be_present - expect(errors[:password_doesnt_match]).to be_present + expect(errors[:confirm_password]).to be_present expect(user.password).to eq('oldpass') end From 5ffc32d8a45876d628c6df9fc8e8cc7a2fb69e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 10:30:12 -0500 Subject: [PATCH 13/18] Configure mail gem. --- Gemfile | 1 + Gemfile.lock | 5 +++++ application/api.rb | 1 + application/config/mail.rb | 11 +++++++++++ 4 files changed, 18 insertions(+) create mode 100644 application/config/mail.rb diff --git a/Gemfile b/Gemfile index 78515d1..60ccb0c 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem 'jwt', '1.5.6' # Json Web Token group :development, :test do gem 'awesome_print', '1.7.0' gem 'pry', '0.10.4' + gem 'letter_opener', '1.4.1' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index f311921..1129212 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,6 +92,10 @@ GEM ice_nine (0.11.2) inflecto (0.0.2) jwt (1.5.6) + launchy (2.4.3) + addressable (~> 2.3) + letter_opener (1.4.1) + launchy (~> 2.2) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -184,6 +188,7 @@ DEPENDENCIES grape-swagger-entity (= 0.1.5) hanami-validations (= 0.6.0) jwt (= 1.5.6) + letter_opener (= 1.4.1) mail (= 2.6.4) mysql2 (= 0.4.5) pry (= 0.10.4) diff --git a/application/api.rb b/application/api.rb index 0f91f16..bdeecff 100644 --- a/application/api.rb +++ b/application/api.rb @@ -21,6 +21,7 @@ class Api < Grape::API; end # Include all config files require 'config/sequel' require 'config/hanami' +require 'config/mail' require 'config/grape' # require some global libs diff --git a/application/config/mail.rb b/application/config/mail.rb new file mode 100644 index 0000000..bfc9490 --- /dev/null +++ b/application/config/mail.rb @@ -0,0 +1,11 @@ +require 'mail' + +Mail.defaults do + if %w(development test).include?(RACK_ENV) + require 'letter_opener' + + delivery_method LetterOpener::DeliveryMethod, :location => File.expand_path('../../../tmp/letter_opener', __FILE__) + else + delivery_method :sendmail + end +end From a454bd61da702cffc074d93220143b5d29f24c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 12:17:08 -0500 Subject: [PATCH 14/18] Setup mailers. --- application/api.rb | 7 +++ application/mailers/mailer_base.rb | 51 +++++++++++++++++++ application/mailers/reset_password_mailer.rb | 17 +++++++ .../mailers/templates/reset_password.html.erb | 7 +++ .../mailers/templates/reset_password.text.erb | 5 ++ .../mailers/templates/welcome_user.html.erb | 1 + .../mailers/templates/welcome_user.text.erb | 1 + application/mailers/welcome_user_mailer.rb | 17 +++++++ 8 files changed, 106 insertions(+) create mode 100644 application/mailers/mailer_base.rb create mode 100644 application/mailers/reset_password_mailer.rb create mode 100644 application/mailers/templates/reset_password.html.erb create mode 100644 application/mailers/templates/reset_password.text.erb create mode 100644 application/mailers/templates/welcome_user.html.erb create mode 100644 application/mailers/templates/welcome_user.text.erb create mode 100644 application/mailers/welcome_user_mailer.rb diff --git a/application/api.rb b/application/api.rb index bdeecff..43ef479 100644 --- a/application/api.rb +++ b/application/api.rb @@ -41,7 +41,14 @@ class Api < Grape::API; end # require all models Dir['./application/models/*.rb'].each { |rb| require rb } +# require all validators Dir['./application/api_validators/**/*.rb'].each { |rb| require rb } + +# require all mailers +require './application/mailers/mailer_base.rb' +Dir['./application/mailers/**/*.rb'].each { |rb| require rb } + + Dir['./application/api_helpers/**/*.rb'].each { |rb| require rb } class Api < Grape::API version 'v1.0', using: :path diff --git a/application/mailers/mailer_base.rb b/application/mailers/mailer_base.rb new file mode 100644 index 0000000..260e3c0 --- /dev/null +++ b/application/mailers/mailer_base.rb @@ -0,0 +1,51 @@ +class Api::MailerBase + include ERB::Util + + def mail(from:, to:, subject:) + mailer = Mail.new do + to to + from from + subject subject + end + + if template?(:text) + mailer.text_part = Mail::Part.new + mailer.text_part.body = render_template(:text) + end + + if template?(:html) + mailer.html_part = Mail::Part.new + mailer.html_part.content_type = 'text/html; charset=UTF-8' + mailer.html_part.body = render_template(:html) + end + + mailer.deliver + end + + def template_name + raise NotImplementedError + end + + protected + + def template?(extension) + template_exists?(template_path(extension)) + end + + def render_template(extension) + render(File.read(template_path(extension))) + end + + def template_path(extension) + File.expand_path("../templates/#{template_name}.#{extension}.erb", __FILE__) + end + + def template_exists?(path) + File.exists?(path) + end + + def render(template) + ERB.new(template).result(binding) + end + +end diff --git a/application/mailers/reset_password_mailer.rb b/application/mailers/reset_password_mailer.rb new file mode 100644 index 0000000..85ebc40 --- /dev/null +++ b/application/mailers/reset_password_mailer.rb @@ -0,0 +1,17 @@ +class Api::ResetPasswordMailer < Api::MailerBase + + def perform(user_id) + @user = Api::Models::User.where(id: user_id).first + + mail( + to: @user.email, + from: 'no-reply@sample.com', + subject: 'Your password was reset!' + ) + end + + def template_name + 'reset_password' + end + +end diff --git a/application/mailers/templates/reset_password.html.erb b/application/mailers/templates/reset_password.html.erb new file mode 100644 index 0000000..357214a --- /dev/null +++ b/application/mailers/templates/reset_password.html.erb @@ -0,0 +1,7 @@ +

Password Reset Confirmation

+ +

+ Hi<%= @user.full_name %>, + + Your password was reset. +

diff --git a/application/mailers/templates/reset_password.text.erb b/application/mailers/templates/reset_password.text.erb new file mode 100644 index 0000000..879c095 --- /dev/null +++ b/application/mailers/templates/reset_password.text.erb @@ -0,0 +1,5 @@ +Password Reset Confirmation + +Hi<%= @user.full_name %>, + +Your password was reset. diff --git a/application/mailers/templates/welcome_user.html.erb b/application/mailers/templates/welcome_user.html.erb new file mode 100644 index 0000000..5288a72 --- /dev/null +++ b/application/mailers/templates/welcome_user.html.erb @@ -0,0 +1 @@ +

Welcome <%= @user.full_name %>!

diff --git a/application/mailers/templates/welcome_user.text.erb b/application/mailers/templates/welcome_user.text.erb new file mode 100644 index 0000000..8e0c013 --- /dev/null +++ b/application/mailers/templates/welcome_user.text.erb @@ -0,0 +1 @@ +Welcome <%= @user.full_name %>! diff --git a/application/mailers/welcome_user_mailer.rb b/application/mailers/welcome_user_mailer.rb new file mode 100644 index 0000000..d766445 --- /dev/null +++ b/application/mailers/welcome_user_mailer.rb @@ -0,0 +1,17 @@ +class Api::WelcomeUserMailer < Api::MailerBase + + def perform(user_id) + @user = Api::Models::User.where(id: user_id).first + + mail( + to: @user.email, + from: 'no-reply@sample.com', + subject: 'Welcome!' + ) + end + + def template_name + 'welcome_user' + end + +end From 4f050a244cf8532a6331e933c9f3b819cb6a7372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 12:31:39 -0500 Subject: [PATCH 15/18] Send mailers in background. --- Gemfile | 2 ++ Gemfile.lock | 11 +++++++++++ Makefile | 3 +++ application/api.rb | 1 + application/api/users.rb | 4 ++++ application/config/yaml/sidekiq.yml | 1 + application/mailers/mailer_base.rb | 3 +++ application/mailers/templates/reset_password.html.erb | 2 +- application/mailers/templates/reset_password.text.erb | 2 +- 9 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 60ccb0c..80ad15c 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,8 @@ gem 'ability_list', '0.0.4' gem 'activesupport', '5.0.0' gem 'bcrypt', '3.1.11' # encryption gem 'jwt', '1.5.6' # Json Web Token +gem 'sidekiq', '4.2.9' # handle background jobs +gem 'redis', '3.3.3' group :development, :test do gem 'awesome_print', '1.7.0' diff --git a/Gemfile.lock b/Gemfile.lock index 1129212..df1ab75 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,6 +20,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) @@ -124,12 +125,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) @@ -148,6 +152,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) @@ -196,9 +205,11 @@ DEPENDENCIES rack-indifferent (= 1.1) rack-test (= 0.6.3) rake (= 11.2.2) + redis (= 3.3.3) 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) diff --git a/Makefile b/Makefile index 3f7042b..7e24daf 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,9 @@ install: run: bundle exec rerun -b --pattern '{Gemfile,Gemfile.lock,.gems,.bundle,.env*,config.ru,**/*.{rb,ru,yml}}' -- thin start --port=3000 --threaded +worker: + bundle exec sidekiq -C ./application/config/yaml/sidekiq.yml + console: bundle exec pry -r ./application/api diff --git a/application/api.rb b/application/api.rb index 43ef479..7bcfa5b 100644 --- a/application/api.rb +++ b/application/api.rb @@ -13,6 +13,7 @@ require 'bundler' Bundler.setup :default, RACK_ENV require 'rack/indifferent' +require 'sidekiq' require 'grape' require 'grape/batch' # Initialize the application so we can add all our components to it diff --git a/application/api/users.rb b/application/api/users.rb index 1844dda..f206732 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -25,6 +25,8 @@ class Api user = Api::Models::User.new(result.output) user.save + Api::WelcomeUserMailer.perform_async(user.id) + present user, with: Api::Entities::User end @@ -70,6 +72,8 @@ class Api user.password = result.output['new_password'] user.save + Api::ResetPasswordMailer.perform_async(user.id) + api_response({status: :ok}) end diff --git a/application/config/yaml/sidekiq.yml b/application/config/yaml/sidekiq.yml index 071bd80..f233d4b 100644 --- a/application/config/yaml/sidekiq.yml +++ b/application/config/yaml/sidekiq.yml @@ -2,6 +2,7 @@ :queues: - [priority, 7] - [default, 5] + - [mailers, 4] - [seldom, 3] staging: :concurrency: 10 diff --git a/application/mailers/mailer_base.rb b/application/mailers/mailer_base.rb index 260e3c0..e14a9a8 100644 --- a/application/mailers/mailer_base.rb +++ b/application/mailers/mailer_base.rb @@ -1,6 +1,9 @@ class Api::MailerBase include ERB::Util + include Sidekiq::Worker + sidekiq_options queue: 'mailers' + def mail(from:, to:, subject:) mailer = Mail.new do to to diff --git a/application/mailers/templates/reset_password.html.erb b/application/mailers/templates/reset_password.html.erb index 357214a..6a58f74 100644 --- a/application/mailers/templates/reset_password.html.erb +++ b/application/mailers/templates/reset_password.html.erb @@ -1,7 +1,7 @@

Password Reset Confirmation

- Hi<%= @user.full_name %>, + Hi <%= @user.full_name %>, Your password was reset.

diff --git a/application/mailers/templates/reset_password.text.erb b/application/mailers/templates/reset_password.text.erb index 879c095..5520052 100644 --- a/application/mailers/templates/reset_password.text.erb +++ b/application/mailers/templates/reset_password.text.erb @@ -1,5 +1,5 @@ Password Reset Confirmation -Hi<%= @user.full_name %>, +Hi <%= @user.full_name %>, Your password was reset. From d077b9e0bb500a7958ebe3cca2c8f66e1bd28f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 12:42:03 -0500 Subject: [PATCH 16/18] Specs for mailers enqueued. --- application/spec/api/users/post_spec.rb | 12 ++++++++++++ application/spec/api/users/reset_password_spec.rb | 12 ++++++++++++ application/spec/spec_helper.rb | 12 ++++++++++++ 3 files changed, 36 insertions(+) diff --git a/application/spec/api/users/post_spec.rb b/application/spec/api/users/post_spec.rb index 28c2963..52e9680 100644 --- a/application/spec/api/users/post_spec.rb +++ b/application/spec/api/users/post_spec.rb @@ -19,6 +19,12 @@ expect(user[:email]).to eq(user_params[:email]) end + it 'should enqueue welcome user mailer' do + expect { + post "api/v1.0/users", user_params + }.to change(Api::WelcomeUserMailer.jobs, :size).by(1) + end + end context "with invalid params" do @@ -38,5 +44,11 @@ expect(errors[:first_name]).to be_present end + it 'should not enqueue welcome user mailer' do + expect { + post "api/v1.0/users", user_params + }.to change(Api::WelcomeUserMailer.jobs, :size).by(0) + end + end end diff --git a/application/spec/api/users/reset_password_spec.rb b/application/spec/api/users/reset_password_spec.rb index 8fe06f4..2f2ca71 100644 --- a/application/spec/api/users/reset_password_spec.rb +++ b/application/spec/api/users/reset_password_spec.rb @@ -20,6 +20,12 @@ expect(user.password).to eq(params[:new_password]) end + it 'should enqueue reset password mailer' do + expect { + patch "api/v1.0/users/#{user_id}/reset_password", params + }.to change(Api::ResetPasswordMailer.jobs, :size).by(1) + end + end context "with invalid params" do @@ -36,6 +42,12 @@ expect(user.password).to eq('oldpass') end + it 'should not enqueue reset password mailer' do + expect { + patch "api/v1.0/users/#{user_id}/reset_password", params + }.to change(Api::ResetPasswordMailer.jobs, :size).by(0) + end + end diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index ca657ec..36eee94 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -4,6 +4,7 @@ require './application/api' require 'faker' require 'factory_girl' +require 'sidekiq/testing' # Load up all application files that we'll be testing in the suites Dir['./application/models/**/*.rb'].sort.each { |rb| require rb } @@ -11,6 +12,12 @@ FactoryGirl.definition_file_paths = %w{./application/spec/factories} FactoryGirl.find_definitions +Sidekiq::Testing.fake! + +Mail.defaults do + delivery_method :test +end + # Factory Girl is expecting ActiveRecord class Sequel::Model alias_method :save!, :save @@ -71,6 +78,7 @@ def current_user config.extend RSpecHelpers config.include RSpecHelpers config.include FactoryGirl::Syntax::Methods + config.include Mail::Matchers config.filter_run_excluding :slow config.color = true config.tty = true @@ -87,4 +95,8 @@ def current_user example.run end end + + config.before(:each) do + Mail::TestMailer.deliveries.clear + end end From 941bb1e00b573ef0f1a25fb1085a5e4035c5a000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 13:09:48 -0500 Subject: [PATCH 17/18] Specs for sent emails --- .env.development.sample | 1 + .env.test.sample | 1 + .../mailers/reset_password_mailer_spec.rb | 26 +++++++++++++++++++ .../spec/mailers/welcome_user_mailer_spec.rb | 26 +++++++++++++++++++ application/spec/spec_helper.rb | 5 ++++ 5 files changed, 59 insertions(+) create mode 100644 application/spec/mailers/reset_password_mailer_spec.rb create mode 100644 application/spec/mailers/welcome_user_mailer_spec.rb diff --git a/.env.development.sample b/.env.development.sample index ae2d78d..0b2a1ec 100644 --- a/.env.development.sample +++ b/.env.development.sample @@ -4,3 +4,4 @@ MAIL_URL=smtp://127.0.0.1:1025 SYSTEM_EMAIL=support@sample.com SITE_URL=http://localhost:3000/ HMAC_SECRET=ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY +REDIS_URL=redis://localhost:6379 diff --git a/.env.test.sample b/.env.test.sample index 8a3c122..b1f614e 100644 --- a/.env.test.sample +++ b/.env.test.sample @@ -4,3 +4,4 @@ MAIL_URL=smtp://127.0.0.1:1025 SYSTEM_EMAIL=support@sample.com SITE_URL=http://localhost:3000/ HMAC_SECRET=ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY +REDIS_URL=redis://localhost:6379 diff --git a/application/spec/mailers/reset_password_mailer_spec.rb b/application/spec/mailers/reset_password_mailer_spec.rb new file mode 100644 index 0000000..ff331d6 --- /dev/null +++ b/application/spec/mailers/reset_password_mailer_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +RSpec.describe Api::ResetPasswordMailer do + + before { Sidekiq::Testing.inline! } + after { Sidekiq::Testing.fake! } + + let!(:user) { create(:user) } + + before do + described_class.perform_async(user.id) + end + + it 'should send the email properly' do + expect(last_mail).to have_sent_email.to(user.email).matching_subject(/password/i) + end + + it "should send html part" do + expect(last_mail.html_part).to be_present + end + + it "should send text part" do + expect(last_mail.text_part).to be_present + end + +end diff --git a/application/spec/mailers/welcome_user_mailer_spec.rb b/application/spec/mailers/welcome_user_mailer_spec.rb new file mode 100644 index 0000000..9e65fa4 --- /dev/null +++ b/application/spec/mailers/welcome_user_mailer_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +RSpec.describe Api::WelcomeUserMailer do + + before { Sidekiq::Testing.inline! } + after { Sidekiq::Testing.fake! } + + let!(:user) { create(:user) } + + before do + described_class.perform_async(user.id) + end + + it 'should send the email properly' do + expect(last_mail).to have_sent_email.to(user.email).matching_subject(/welcome/i) + end + + it "should send html part" do + expect(last_mail.html_part).to be_present + end + + it "should send text part" do + expect(last_mail.text_part).to be_present + end + +end diff --git a/application/spec/spec_helper.rb b/application/spec/spec_helper.rb index 36eee94..66b86a2 100644 --- a/application/spec/spec_helper.rb +++ b/application/spec/spec_helper.rb @@ -50,6 +50,10 @@ def get_scope opts = {} scope.instance_variable_set(:@current_user, opts[:as_user]) scope end + + def last_mail + Mail::TestMailer.deliveries.last + end end class Api @@ -98,5 +102,6 @@ def current_user config.before(:each) do Mail::TestMailer.deliveries.clear + Sidekiq::Worker.clear_all end end From 3ac25c5982808b344695301ff42599da2f8d3089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Corcuera?= Date: Sat, 4 Mar 2017 14:05:22 -0500 Subject: [PATCH 18/18] Add documentation to user's endpoints. --- application/api/users.rb | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/application/api/users.rb b/application/api/users.rb index f206732..d207a6b 100644 --- a/application/api/users.rb +++ b/application/api/users.rb @@ -10,12 +10,13 @@ class Api present users, with: Api::Entities::User end + desc "Creates a user" params do - requires :first_name, type: String - requires :last_name, type: String - requires :email, type: String - requires :password, type: String - optional :date_of_birth, type: String + requires :first_name, type: String, desc: "User's first name" + requires :last_name, type: String, desc: "User's last name" + requires :email, type: String, desc: "User's email name" + requires :password, type: String, desc: "User's password" + optional :date_of_birth, type: String, desc: "User's date of birth / Format: YYYY:MM:DD HH:MM:SS TZ" end post do @@ -30,11 +31,12 @@ class Api present user, with: Api::Entities::User end + desc "Updates a user" params do - optional :first_name, type: String - optional :last_name, type: String - optional :email, type: String - optional :date_of_birth, type: String + optional :first_name, type: String, desc: "User's first name" + optional :last_name, type: String, desc: "User's last name" + optional :email, type: String, desc: "User's email name" + optional :date_of_birth, type: String, desc: "User's date of birth / Format: YYYY:MM:DD HH:MM:SS TZ" end put '/:id' do @@ -56,9 +58,10 @@ class Api present user, with: Api::Entities::User end + desc "Resets a user's password" params do - requires :new_password, type: String - requires :confirm_password, type: String + requires :new_password, type: String, desc: "New password" + requires :confirm_password, type: String, desc: "Password confirmation" end patch '/:id/reset_password' do @@ -77,9 +80,10 @@ class Api api_response({status: :ok}) end + desc "Signin a user" params do - requires :email, type: String - requires :password, type: String + requires :email, type: String, desc: "User's email" + requires :password, type: String, desc: "User's password" end post '/signin' do