Skip to content

Commit ce035c7

Browse files
committed
Offload logic to operations
1 parent f684a6a commit ce035c7

17 files changed

Lines changed: 184 additions & 72 deletions

application/api.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,20 @@ class Api < Grape::API; end
3232
require 'lib/time_formats'
3333
require 'lib/io'
3434
require 'lib/pretty_logger'
35+
require 'lib/operation'
3536

3637
# load active support helpers
3738
require 'active_support'
3839
require 'active_support/core_ext'
3940

4041
# require application classes
42+
require './application/operations/user_operation'
43+
4144
Dir['./application/models/*.rb'].each { |rb| require rb }
4245
Dir['./application/entities/*.rb'].each { |rb| require rb }
4346
Dir['./application/jobs/*.rb'].each { |rb| require rb }
4447
Dir['./application/validators/*.rb'].each { |rb| require rb }
45-
48+
Dir['./application/operations/*.rb'].each { |rb| require rb }
4649
Dir['./application/api_helpers/**/*.rb'].each { |rb| require rb }
4750

4851
class Api < Grape::API
@@ -71,7 +74,7 @@ class Api < Grape::API
7174
Api.logger.error(e.class)
7275
Api.logger.error(e.message)
7376
Api.logger.error(e.backtrace.join("\n"))
74-
error!({ error_type: 'internal' }, 404)
77+
error!({ error_type: 'internal' }, 500)
7578
end
7679

7780
helpers SharedParams

application/api/auth.rb

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,16 @@ class Api
66
params: Models::Login::Input.documentation_in_body,
77
failure: [ { code: 403, message: 'Unauthorized' } ]
88
post 'login' do
9-
user = Models::User[email: params[:email]]
10-
if user.authenticate(params[:password])
11-
{ token: auth_token_for(user) }
12-
else
13-
api_response(error_type: :unauthorized, errors: { reason: "Invalid credentials" })
9+
Login.(params) do
10+
ok { |user| present :token, auth_token_for(user) }
11+
fail { |errors| api_response errors }
1412
end
1513
end
1614

1715
desc 'Generates a new reset password code',
1816
success: { code: 204 }
1917
post 'reset_password_code/:user_id' do
20-
user = Models::User.with_pk!(params[:user_id])
21-
code = user.update_reset_password_code!
22-
23-
SendResetPasswordCodeJob.perform_async(user.email, code)
24-
18+
NewResetPasswordCode.(params)
2519
body false
2620
end
2721

application/api/users.rb

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,21 @@ class Api
1515
params: Models::User::Input.documentation_in_body,
1616
failure: [ { code: 422, message: 'Invalid input' } ]
1717
post do
18-
result = UserValidator.new(params).validate
19-
20-
if result.success?
21-
@user = Models::User.create(result.output)
22-
ConfirmNewUserJob.perform_async(@user.email)
23-
24-
present @user
25-
else
26-
api_response(error_type: :invalid, errors: result.messages)
18+
CreateUser.(params) do
19+
ok { |user| present user }
20+
fail { |errors| api_response errors }
2721
end
2822
end
2923

3024
route_param :id do
31-
before do
32-
@user = Models::User.with_pk!(params[:id])
33-
end
34-
3525
desc "Resets a user's password",
3626
params: Models::PasswordReset::Input.documentation_in_body,
3727
success: { code: 204 },
3828
failure: [ { code: 422, message: 'Invalid input' }, { code: 401, message: 'Invalid verification code' } ]
3929
patch :reset_password do
40-
result = ResetPasswordValidator.new(params).validate
41-
42-
if !@user.valid_reset_password_code?(result.output[:verification_code])
43-
api_response(error_type: :unauthorized, errors: { reason: "Invalid code" })
44-
elsif result.failure?
45-
api_response(error_type: :invalid, errors: result.messages)
46-
else
47-
@user.update(password: result.output[:new_password])
48-
ConfirmResetPasswordJob.perform_async(@user.email)
49-
50-
body false
30+
ResetPassword.(params) do
31+
ok { body false }
32+
fail { |errors| api_response errors }
5133
end
5234
end
5335

@@ -57,16 +39,9 @@ class Api
5739
failure: [ { code: 422, message: 'Invalid input' }, { code: 403, message: 'Unauthorized operation attempt' } ],
5840
headers: { 'Authorization' => { description: 'JWT Authorization Token', required: true } }
5941
put do
60-
result = UserValidator.new(params).validate
61-
62-
if current_user.nil? || current_user.cannot?(:edit, @user)
63-
api_response(error_type: :forbidden, errors: { reason: "Permission denied" })
64-
elsif result.failure?
65-
api_response(error_type: :invalid, errors: result.messages)
66-
else
67-
@user.update(result.output)
68-
69-
present @user
42+
UpdateUser.(current_user, params) do
43+
ok { |user| present user }
44+
fail { |errors| api_response errors }
7045
end
7146
end
7247
end

application/lib/operation.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
class Operation
2+
class Validator
3+
include Hanami::Validations::Form
4+
predicates FormPredicates
5+
end
6+
7+
class Result
8+
attr_reader :value, :errors
9+
10+
def initialize(value: nil, errors: {})
11+
@value, @errors = value, errors
12+
end
13+
14+
def succesfull?
15+
@errors.empty?
16+
end
17+
18+
def failure?
19+
!succesful?
20+
end
21+
end
22+
23+
class Responder
24+
def self.respond(response, &bl)
25+
new(response, &bl).respond
26+
end
27+
28+
def initialize(response, &bl)
29+
instance_eval(&bl)
30+
@response, @context = response, bl.binding.receiver
31+
end
32+
33+
def ok(&bl)
34+
@ok = bl
35+
end
36+
37+
def fail(&bl)
38+
@fail = bl
39+
end
40+
41+
def respond
42+
if @response.succesfull?
43+
@context.instance_exec(@response.value, &@ok)
44+
else
45+
@context.instance_exec(@response.errors, &@fail)
46+
end
47+
end
48+
end
49+
50+
def self.call(*args, &bl)
51+
result = new.call(*args)
52+
block_given? ? Responder.respond(result, &bl) : result
53+
end
54+
55+
def success(value)
56+
Result.new(value: value)
57+
end
58+
59+
def failure(errors)
60+
Result.new(errors: errors)
61+
end
62+
end

application/models/user.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,6 @@ def abilities
1414
def full_name
1515
"#{self.first_name} #{self.last_name}"
1616
end
17-
18-
def update_reset_password_code!
19-
SecureRandom.hex(5).upcase.tap do |code|
20-
update(reset_password: code, reset_password_expiration: Time.now + 30.minutes)
21-
end
22-
end
23-
24-
def valid_reset_password_code?(code)
25-
reset_password && code == reset_password && Time.now <= reset_password_expiration
26-
end
2717
end
2818
end
2919
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class Api
2+
class CreateUser < UserOperation
3+
def call(params)
4+
val = UserValidator.new(params).validate
5+
6+
if val.success?
7+
user = Models::User.create(val.output)
8+
ConfirmNewUserJob.perform_async(user.email)
9+
success(user)
10+
else
11+
failure(error_type: :invalid, messages: val.messages)
12+
end
13+
end
14+
end
15+
end

application/operations/login.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Api
2+
class Login < Operation
3+
def call(params)
4+
user = Models::User.first!(email: params[:email])
5+
6+
if user.authenticate(params[:password])
7+
success(user)
8+
else
9+
failure(error_type: :unauthorized)
10+
end
11+
end
12+
end
13+
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Api
2+
class NewResetPasswordCode < Operation
3+
def call(params)
4+
user = Models::User.with_pk!(params[:user_id])
5+
code = new_reset_password_code!(user)
6+
SendResetPasswordCodeJob.perform_async(user.email, code)
7+
success(user)
8+
end
9+
10+
private
11+
12+
def new_reset_password_code!(user)
13+
SecureRandom.hex(4).upcase.tap do |code|
14+
user.update(reset_password: code, reset_password_expiration: Time.now + 30.minutes)
15+
end
16+
end
17+
end
18+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class Api
2+
class ResetPassword < UserOperation
3+
def call(params)
4+
user = user_for(params)
5+
val = ResetPasswordValidator.new(params).validate
6+
7+
if !valid_reset_password_code?(user, val.output[:verification_code])
8+
failure(error_type: :unauthorized)
9+
elsif val.failure?
10+
failure(error_type: :invalid, errors: val.messages)
11+
else
12+
user.update(password: val.output[:new_password])
13+
ConfirmResetPasswordJob.perform_async(user.email)
14+
success(user)
15+
end
16+
end
17+
18+
private
19+
20+
def valid_reset_password_code?(user, code)
21+
code && code == user.reset_password && Time.now <= user.reset_password_expiration
22+
end
23+
end
24+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class Api
2+
class UpdateUser < UserOperation
3+
def call(current_user, params)
4+
user = user_for(params)
5+
val = UserValidator.new(params).validate
6+
7+
if current_user.nil? || current_user.cannot?(:edit, user)
8+
failure(error_type: :forbidden)
9+
elsif val.failure?
10+
failure(error_type: :invalid, messages: val.messages)
11+
else
12+
user.update(val.output)
13+
success(user)
14+
end
15+
end
16+
end
17+
end

0 commit comments

Comments
 (0)