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
5 changes: 5 additions & 0 deletions .env.development.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ 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/
SYSTEM_EMAIL=support@sample.com
SITE_URL=http://localhost:3000/
SMTP_SERVER=mail.example.com
MAIL_SMTP_USER=user@example.com
MAIL_SMTP_PASSWORD=example
6 changes: 4 additions & 2 deletions .env.test.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
RACK_ENV=development
RACK_ENV=test
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/
SMTP_SERVER=mail.example.com
MAIL_SMTP_USER=user@example.com
MAIL_SMTP_PASSWORD=example
16 changes: 15 additions & 1 deletion application/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ class Api < Grape::API; end
# load active support helpers
require 'active_support'
require 'active_support/core_ext'
require 'mail'

# require all models
Dir['./application/models/*.rb'].each { |rb| require rb }

Dir['./application/validations/*.rb'].each { |rb| require rb }
Dir['./application/api_helpers/**/*.rb'].each { |rb| require rb }
class Api < Grape::API
version 'v1.0', using: :path
Expand Down Expand Up @@ -63,4 +64,17 @@ class Api < Grape::API

add_swagger_documentation \
mount_path: '/docs'

Mail.defaults do
delivery_method :smtp, {
address: SMTP_SERVER,
port: 465,
user_name: MAIL_SMTP_USER,
password: MAIL_SMTP_PASSWORD,
authentication: :login,
ssl: true,
tls: true,
enable_starttls_auto: true
}
end
end
52 changes: 52 additions & 0 deletions application/api/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,57 @@ class Api
data: users
}
end

post do
result = CreateUserValidation.new(params).validate
if result.success? && user = Models::User.create(result.output)
mail = ::Mail.new do
from SYSTEM_EMAIL
to user.email
subject 'Your account has been created successfully'
body 'Need a nice body'
end.deliver!
Entities::User.represent(user)
else
error!(result.messages, 400)
end
end

put ':id' do
authenticate!
user = Models::User[params[:id]]

return error!('Unauthorized', 401) unless current_user.can?(:edit, user)

result = EditUserValidation.new(params).validate
if result.success?
user.update(result.output)
Entities::User.represent(user)
else
error!(result.messages, 400)
end
end

patch ':id/reset_password' do
authenticate!
user = Models::User[params[:id]]

return error!('Unauthorized', 401) unless current_user.can?(:edit, user)

result = ResetPasswordUserValidation.new(params).validate

if result.success?
user.update(password: result.output[:new_password])
mail = ::Mail.new do
from SYSTEM_EMAIL
to user.email
subject 'Your password has been reset successfully'
body 'Need a nice body'
end.deliver!
Entities::User.represent(user)
else
error!(result.messages, 400)
end
end
end
end
18 changes: 18 additions & 0 deletions application/api_entities/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Api
module Entities
class User < Grape::Entity
format_with(:iso_timestamp) { |dt| dt.iso8601 }

expose :id
expose :first_name
expose :last_name
expose :email

with_options(format_with: :iso_timestamp) do
expose :born_on, if: :born_on
expose :created_at
expose :updated_at
end
end
end
end
13 changes: 10 additions & 3 deletions application/api_helpers/auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ module Auth

module HelperMethods
def authenticate!
# Library to authenticate user can go here
error!('Unauthorized. Invalid or expired token.', 401) unless set_current_user
end

def current_user
@current_user
private

def set_current_user
token = Api::Models::UserToken.first(access_token: headers['Token'])
if token && !token.expired?
Api.class_variable_set(:@@current_user, Api::Models::User[token.user_id])
else
false
end
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion application/config/variables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
SUPPORT_EMAIL = ENV.fetch('SUPPORT_EMAIL').freeze
INTERNAL_EMAIL = ENV.fetch('INTERNAL_EMAIL').freeze
DATABASE_URL = ENV.fetch('DATABASE_URL').freeze
MAIL_URL = ENV.fetch('MAIL_URL').freeze
SMTP_SERVER = ENV.fetch('SMTP_SERVER').freeze
MAIL_SMTP_USER = ENV.fetch('MAIL_SMTP_USER').freeze
MAIL_SMTP_PASSWORD = ENV.fetch('MAIL_SMTP_PASSWORD').freeze
SYSTEM_EMAIL = ENV.fetch('SYSTEM_EMAIL').freeze
SITE_URL = ENV.fetch('SITE_URL').freeze
15 changes: 15 additions & 0 deletions application/migrate/1488320988_add_user_tokens_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Sequel.migration do
change do
create_table(:user_tokens) do
primary_key :id

String :access_token
DateTime :expires_at
Integer :user_id
Boolean :active

index :user_id, unique: false
index :access_token, unique: true
end
end
end
18 changes: 18 additions & 0 deletions application/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,31 @@ module Models
class User < Sequel::Model(:users)
include AbilityList::Helpers

one_to_many :user_tokens

def after_create
update_token!
end

def abilities
@abilities ||= Abilities.new(self)
end

def full_name
"#{self.first_name} #{self.last_name}"
end

def token
user_tokens.last.access_token
end

def update_token!
user_tokens << UserToken.create(user_id: self.id)
end

def password_confirmation=(pass)
nil
end
end
end
end
25 changes: 25 additions & 0 deletions application/models/user_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class Api
module Models
class UserToken < Sequel::Model(:user_tokens)
many_to_one :user

def before_create
generate_access_token
set_expiration
end

def expired?
DateTime.now >= self.expires_at
end

private
def generate_access_token
self.access_token = SecureRandom.hex
end

def set_expiration
self.expires_at = DateTime.now + 1.day
end
end
end
end
42 changes: 42 additions & 0 deletions application/spec/api/users/patch_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'spec_helper'

describe 'PATCH /api/users' do
before :all do
@user = create :user
password = Faker::Internet.password
@user_attributes = {
new_password: password,
new_password_confirmation: password
}
end

it 'should update user password' do
header('token', @user.token)
patch("api/v1.0/users/#{@user.id}/reset_password", @user_attributes)
expect(last_response.status).to eq(200)
end

it 'should NOT update user password, invalid token' do
header('token', @user.token)
@user2 = create :user
patch("api/v1.0/users/#{@user2.id}/reset_password", @user_attributes)
expect(last_response.status).to eq(401)
end

it 'missing params, should NOT update user password' do
header('token', @user.token)
patch("api/v1.0/users/#{@user.id}/reset_password", {})
body = response_body
expect(last_response.status).to eq(400)
expect(body.keys).to eq([:new_password])
end

it 'mismatch passwords, should NOT update user password' do
@user_attributes[:new_password] = Faker::Internet.password
header('token', @user.token)
patch("api/v1.0/users/#{@user.id}/reset_password", @user_attributes)
body = response_body
expect(last_response.status).to eq(400)
expect(body.keys).to eq([:new_password_confirmation])
end
end
41 changes: 41 additions & 0 deletions application/spec/api/users/post_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'spec_helper'

describe 'POST /api/users' do
before :all do
password = Faker::Internet.password
@user_attributes = {
first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
email: Faker::Internet.email,
password: password,
password_confirmation: password,
born_on: Faker::Date.birthday.to_time
}
end

it 'should create a new user' do
post('api/v1.0/users', @user_attributes)
expect(last_response.status).to eq(201)
end

it 'should create a new user without born_on' do
@user_attributes.delete(:born_on)
post('api/v1.0/users', @user_attributes)
expect(last_response.status).to eq(201)
end

it 'missing params, should NOT create a new user' do
post('api/v1.0/users', {})
body = response_body
expect(last_response.status).to eq(400)
expect(body.keys).to eq([:first_name, :last_name, :email, :password])
end

it 'wrong born_on format, should NOT create a new user' do
@user_attributes[:born_on] = Faker::Date.birthday.to_s
post('api/v1.0/users', @user_attributes)
body = response_body
expect(last_response.status).to eq(400)
expect(body).to eq(born_on: ['must be in format YYYY-MM-DD HH:MM:SS'])
end
end
43 changes: 43 additions & 0 deletions application/spec/api/users/put_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'spec_helper'

describe 'PUT /api/users' do
before :all do
@user = create :user
@user_attributes = {
first_name: Faker::Name.first_name,
last_name: Faker::Name.last_name,
email: Faker::Internet.email,
born_on: Faker::Date.birthday.to_time
}
end

it 'should update user attributes' do
header('token', @user.token)
put("api/v1.0/users/#{@user.id}", @user_attributes)
expect(last_response.status).to eq(200)
end

it 'should NOT update user attributes, invalid token' do
header('token', @user.token)
@user2 = create :user
put("api/v1.0/users/#{@user2.id}", @user_attributes)
expect(last_response.status).to eq(401)
end

it 'missing params, should NOT update user attributes' do
header('token', @user.token)
put("api/v1.0/users/#{@user.id}", {})
body = response_body
expect(last_response.status).to eq(400)
expect(body.keys).to eq([:first_name, :last_name, :email])
end

it 'wrong born_on format, should NOT update user attributes' do
@user_attributes[:born_on] = Faker::Date.birthday.to_s
header('token', @user.token)
put("api/v1.0/users/#{@user.id}", @user_attributes)
body = response_body
expect(last_response.status).to eq(400)
expect(body).to eq(born_on: ['must be in format YYYY-MM-DD HH:MM:SS'])
end
end
4 changes: 4 additions & 0 deletions application/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
require 'faker'
require 'factory_girl'

Mail.defaults do
delivery_method :test
end

# Load up all application files that we'll be testing in the suites
Dir['./application/models/**/*.rb'].sort.each { |rb| require rb }

Expand Down
12 changes: 12 additions & 0 deletions application/validations/create_user_validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class CreateUserValidation
include Hanami::Validations::Form
predicates FormPredicates

validations do
required(:first_name).filled(:str?)
required(:last_name).filled(:str?)
required(:email).filled(:str?, :email?)
required(:password).filled.confirmation
optional(:born_on).filled(:datetime_str?)
end
end
11 changes: 11 additions & 0 deletions application/validations/edit_user_validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class EditUserValidation
include Hanami::Validations::Form
predicates FormPredicates

validations do
required(:first_name).maybe(:str?)
required(:last_name).maybe(:str?)
required(:email).maybe(:str?, :email?)
optional(:born_on).maybe(:datetime_str?)
end
end
Loading