Skip to content

Commit fe3bc1e

Browse files
committed
Add user admin features
1 parent 047ec45 commit fe3bc1e

37 files changed

Lines changed: 652 additions & 59 deletions

.env.development.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ DATABASE_URL=mysql2://root:@127.0.0.1:3306/sample?reconnect=true
33
MAIL_URL=smtp://127.0.0.1:1025
44
SYSTEM_EMAIL=support@sample.com
55
SITE_URL=http://localhost:3000/
6+
HMAC_SECRET=57f1bcf21caed1930fba8ac4ef74b1636d80bb9347b5af3863e8897fe10a98eded469734909903c5a3c166fec8d536b81b3636eda644c5c6a1a79b83a193d59e

.env.test.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ DATABASE_URL=mysql2://root:@127.0.0.1:3306/sample_test?reconnect=true
33
MAIL_URL=smtp://127.0.0.1:1025
44
SYSTEM_EMAIL=support@sample.com
55
SITE_URL=http://localhost:3000/
6+
HMAC_SECRET=a53d4a36f4bf1e08e14a0cbd24401856aaeac17f96311d199f51c90c100bfb4f175e679017dd9125b903cc97aa0561e7b533154f76681df00bd97336ec6c9edc

.rspec

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
--color
2+
--require spec_helper
3+
-I ./application/spec

Gemfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,24 @@ gem 'grape-swagger-entity', '0.1.5' # parse entities in api
1414
gem 'rack-indifferent', '1.1' # makes param keys symbols
1515
gem 'mysql2', '0.4.5'
1616
gem 'sequel', '4.40.0'
17+
gem 'sequel_secure_password'
1718
gem 'mail', '2.6.4'
1819
gem 'uuidtools', ' 2.1.5'
1920
gem 'hanami-validations', '0.6.0' # form validation
2021
gem 'dry-validation', '0.10.4' # validation methods for reform
2122
gem 'ability_list', '0.0.4'
2223
gem 'activesupport', '5.0.0'
24+
gem 'sucker_punch'
25+
gem 'jwt'
2326

2427
group :development, :test do
2528
gem 'awesome_print', '1.7.0'
2629
gem 'pry', '0.10.4'
30+
gem 'pry-doc'
31+
gem 'pry-byebug'
2732
end
2833

2934
group :test do
30-
gem 'webmock', '2.1.0'
31-
gem 'vcr', '3.0.3'
32-
gem 'database_cleaner', '1.5.3'
3335
gem 'factory_girl', '4.7.0'
3436
gem 'faker', '1.6.6'
3537
gem 'rack-test', '0.6.3'

Gemfile.lock

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@ GEM
77
i18n (~> 0.7)
88
minitest (~> 5.1)
99
tzinfo (~> 1.1)
10-
addressable (2.5.0)
11-
public_suffix (~> 2.0, >= 2.0.2)
1210
awesome_print (1.7.0)
1311
axiom-types (0.1.1)
1412
descendants_tracker (~> 0.0.4)
1513
ice_nine (~> 0.11.0)
1614
thread_safe (~> 0.3, >= 0.3.1)
15+
bcrypt (3.1.11)
1716
builder (3.2.2)
17+
byebug (9.0.6)
1818
coderay (1.1.1)
1919
coercible (1.0.0)
2020
descendants_tracker (~> 0.0.1)
2121
concurrent-ruby (1.0.4)
22-
crack (0.4.3)
23-
safe_yaml (~> 1.0.0)
2422
daemons (1.2.4)
25-
database_cleaner (1.5.3)
2623
descendants_tracker (0.0.4)
2724
thread_safe (~> 0.3, >= 0.3.1)
2825
diff-lcs (1.2.5)
@@ -31,7 +28,7 @@ GEM
3128
dry-container (0.6.0)
3229
concurrent-ruby (~> 1.0)
3330
dry-configurable (~> 0.1, >= 0.1.3)
34-
dry-core (0.2.1)
31+
dry-core (0.2.3)
3532
concurrent-ruby (~> 1.0)
3633
dry-equalizer (0.2.0)
3734
dry-logic (0.4.0)
@@ -85,11 +82,11 @@ GEM
8582
hanami-validations (0.6.0)
8683
dry-validation (~> 0.9)
8784
hanami-utils (~> 0.8)
88-
hashdiff (0.3.1)
8985
hashie (3.4.6)
9086
i18n (0.7.0)
9187
ice_nine (0.11.2)
9288
inflecto (0.0.2)
89+
jwt (1.5.6)
9390
listen (3.1.5)
9491
rb-fsevent (~> 0.9, >= 0.9.4)
9592
rb-inotify (~> 0.9, >= 0.9.7)
@@ -112,7 +109,12 @@ GEM
112109
coderay (~> 1.1.0)
113110
method_source (~> 0.8.1)
114111
slop (~> 3.4)
115-
public_suffix (2.0.4)
112+
pry-byebug (3.4.2)
113+
byebug (~> 9.0)
114+
pry (~> 0.10)
115+
pry-doc (0.9.0)
116+
pry (~> 0.9)
117+
yard (~> 0.8)
116118
rack (1.6.4)
117119
rack-accept (0.4.5)
118120
rack (>= 0.4)
@@ -140,9 +142,13 @@ GEM
140142
rspec-support (~> 3.5.0)
141143
rspec-support (3.5.0)
142144
ruby_dep (1.5.0)
143-
safe_yaml (1.0.4)
144145
sequel (4.40.0)
146+
sequel_secure_password (0.2.12)
147+
bcrypt (>= 3.1, < 4.0)
148+
sequel (>= 4.1.0, < 5.0)
145149
slop (3.6.0)
150+
sucker_punch (2.0.2)
151+
concurrent-ruby (~> 1.0.0)
146152
thin (1.7.0)
147153
daemons (~> 1.0, >= 1.0.9)
148154
eventmachine (~> 1.0, >= 1.0.4)
@@ -152,16 +158,12 @@ GEM
152158
tzinfo (1.2.2)
153159
thread_safe (~> 0.1)
154160
uuidtools (2.1.5)
155-
vcr (3.0.3)
156161
virtus (1.0.5)
157162
axiom-types (~> 0.1)
158163
coercible (~> 1.0)
159164
descendants_tracker (~> 0.0, >= 0.0.3)
160165
equalizer (~> 0.0, >= 0.0.9)
161-
webmock (2.1.0)
162-
addressable (>= 2.3.6)
163-
crack (>= 0.3.2)
164-
hashdiff
166+
yard (0.9.5)
165167

166168
PLATFORMS
167169
ruby
@@ -170,7 +172,6 @@ DEPENDENCIES
170172
ability_list (= 0.0.4)
171173
activesupport (= 5.0.0)
172174
awesome_print (= 1.7.0)
173-
database_cleaner (= 1.5.3)
174175
dry-validation (= 0.10.4)
175176
factory_girl (= 4.7.0)
176177
faker (= 1.6.6)
@@ -180,20 +181,23 @@ DEPENDENCIES
180181
grape-swagger (= 0.25.1)
181182
grape-swagger-entity (= 0.1.5)
182183
hanami-validations (= 0.6.0)
184+
jwt
183185
mail (= 2.6.4)
184186
mysql2 (= 0.4.5)
185187
pry (= 0.10.4)
188+
pry-byebug
189+
pry-doc
186190
rack (= 1.6.4)
187191
rack-indifferent (= 1.1)
188192
rack-test (= 0.6.3)
189193
rake (= 11.2.2)
190194
rerun (= 0.11.0)
191195
rspec (= 3.5.0)
192196
sequel (= 4.40.0)
197+
sequel_secure_password
198+
sucker_punch
193199
thin (= 1.7.0)
194200
uuidtools (= 2.1.5)
195-
vcr (= 3.0.3)
196-
webmock (= 2.1.0)
197201

198202
RUBY VERSION
199203
ruby 2.3.3p222

application/api.rb

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,45 @@
1515
require 'rack/indifferent'
1616
require 'grape'
1717
require 'grape/batch'
18+
require 'sucker_punch'
19+
require 'mail'
20+
require 'jwt'
1821
# Initialize the application so we can add all our components to it
1922
class Api < Grape::API; end
2023

2124
# Include all config files
2225
require 'config/sequel'
2326
require 'config/hanami'
2427
require 'config/grape'
28+
require 'config/mail'
2529

2630
# require some global libs
2731
require 'lib/core_ext'
2832
require 'lib/time_formats'
2933
require 'lib/io'
34+
require 'lib/pretty_logger'
3035

3136
# load active support helpers
3237
require 'active_support'
3338
require 'active_support/core_ext'
3439

35-
# require all models
40+
# require application classes
3641
Dir['./application/models/*.rb'].each { |rb| require rb }
42+
Dir['./application/entities/*.rb'].each { |rb| require rb }
43+
Dir['./application/jobs/*.rb'].each { |rb| require rb }
44+
Dir['./application/validators/*.rb'].each { |rb| require rb }
3745

3846
Dir['./application/api_helpers/**/*.rb'].each { |rb| require rb }
47+
3948
class Api < Grape::API
4049
version 'v1.0', using: :path
4150
content_type :json, 'application/json'
51+
content_type :txt, 'text/plain'
4252
default_format :json
4353
prefix :api
54+
55+
logger PrettyLogger.logger
56+
4457
rescue_from Grape::Exceptions::ValidationErrors do |e|
4558
ret = { error_type: 'validation', errors: {} }
4659
e.each do |x, err|
@@ -50,17 +63,30 @@ class Api < Grape::API
5063
error! ret, 400
5164
end
5265

66+
rescue_from Sequel::NoMatchingRow do |e|
67+
error!({ error_type: 'not_found' }, 404)
68+
end
69+
70+
rescue_from :all do |e|
71+
Api.logger.error(e.class)
72+
Api.logger.error(e.message)
73+
Api.logger.error(e.backtrace.join("\n"))
74+
error!({ error_type: 'internal' }, 404)
75+
end
76+
5377
helpers SharedParams
5478
helpers ApiResponse
5579
include Auth
5680

5781
before do
82+
header['Access-Control-Allow-Origin'] = '*'
83+
header['Access-Control-Request-Method'] = '*'
84+
5885
authenticate!
5986
end
6087

6188
Dir['./application/api_entities/**/*.rb'].each { |rb| require rb }
6289
Dir['./application/api/**/*.rb'].each { |rb| require rb }
6390

64-
add_swagger_documentation \
65-
mount_path: '/docs'
91+
add_swagger_documentation mount_path: '/docs'
6692
end

application/api/auth.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
class Api
2+
namespace :auth do
3+
4+
desc 'Generates a new authentication token',
5+
entity: Models::Login::Authorization,
6+
params: Models::Login::Input.documentation_in_body,
7+
failure: [ { code: 403, message: 'Unauthorized' } ]
8+
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: ["Invalid credentials"])
14+
end
15+
end
16+
17+
desc 'Generates a new reset password code',
18+
success: { code: 204 }
19+
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+
25+
body false
26+
end
27+
28+
end
29+
end

application/api/users.rb

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,72 @@ class Api
33
params do
44
includes :basic_search
55
end
6+
67
get do
7-
users = SEQUEL_DB[:users].all
8-
{
9-
data: users
10-
}
8+
users = Models::User.all
9+
present :data, users, with: Models::User::Entity
10+
end
11+
12+
13+
desc 'Creates a new user',
14+
entity: Models::User::Entity,
15+
params: Models::User::Input.documentation_in_body,
16+
failure: [ { code: 422, message: 'Invalid input' } ]
17+
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)
27+
end
28+
end
29+
30+
route_param :id do
31+
before do
32+
@user = Models::User.with_pk!(params[:id])
33+
end
34+
35+
desc "Resets a user's password",
36+
params: Models::PasswordReset::Input.documentation_in_body,
37+
success: { code: 204 },
38+
failure: [ { code: 422, message: 'Invalid input' }, { code: 401, message: 'Invalid verification code' } ]
39+
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: ["Invalid verification 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
51+
end
52+
end
53+
54+
desc 'Updates an existing user',
55+
entity: Models::User::Entity,
56+
params: Models::User::Input.documentation_in_body,
57+
failure: [ { code: 422, message: 'Invalid input' }, { code: 403, message: 'Unauthorized operation attempt' } ],
58+
headers: { 'Authorization' => { description: 'JWT Authorization Token', required: true } }
59+
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: ["Attempted to edit another user"])
64+
elsif result.failure?
65+
api_response(error_type: :invalid, errors: result.messages)
66+
else
67+
@user.update(result.output)
68+
69+
present @user
70+
end
71+
end
1172
end
1273
end
1374
end

application/api_helpers/api_response.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ def api_response response
99
status 404
1010
when :forbidden
1111
status 403
12+
when :unauthorized
13+
status 401
14+
when :unprocessable_entity, :invalid
15+
status 422
1216
else
1317
status 400
1418
end

application/api_helpers/auth.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,30 @@ module Auth
88

99
module HelperMethods
1010
def authenticate!
11-
# Library to authenticate user can go here
11+
if token = headers['Authorization']
12+
@current_user = Models::User.with_pk(extract_user_id(token))
13+
end
14+
rescue JWT::DecodeError
15+
error!({error_type: :unauthorized}, :unauthorized)
1216
end
1317

1418
def current_user
1519
@current_user
1620
end
21+
22+
def extract_user_id(token)
23+
JWT.decode(token, HMAC_SECRET, true, algorithm: 'HS256')[0]['user_id']
24+
end
25+
26+
def auth_token_for(user)
27+
payload = {
28+
iss: "ruby-api-example",
29+
exp: Time.now.to_i + 4.hours,
30+
user_id: user.id
31+
}
32+
33+
JWT.encode(payload, HMAC_SECRET, 'HS256')
34+
end
1735
end
1836
end
1937
end

0 commit comments

Comments
 (0)