actn-api 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env +2 -0
- data/.gitignore +17 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/Procfile +11 -0
- data/README.md +130 -0
- data/Rakefile +16 -0
- data/actn-api.gemspec +33 -0
- data/apis/core/backend.rb +103 -0
- data/apis/core/frontend.rb +46 -0
- data/apis/public/connect.rb +40 -0
- data/apis/public/delete.rb +15 -0
- data/apis/public/query.rb +26 -0
- data/apis/public/upsert.rb +16 -0
- data/bin/actn-api +36 -0
- data/config/common.rb +16 -0
- data/config/core.rb +1 -0
- data/config/haproxy.cfg +73 -0
- data/config/public.rb +5 -0
- data/db/1_api.sql +9 -0
- data/db/schemas/client.json +41 -0
- data/db/schemas/user.json +23 -0
- data/lib/actn/api/client.rb +92 -0
- data/lib/actn/api/core.rb +35 -0
- data/lib/actn/api/goliath/params.rb +70 -0
- data/lib/actn/api/goliath/validator.rb +54 -0
- data/lib/actn/api/mw/auth.rb +122 -0
- data/lib/actn/api/mw/cors.rb +58 -0
- data/lib/actn/api/mw/no_xss.rb +28 -0
- data/lib/actn/api/public.rb +89 -0
- data/lib/actn/api/user.rb +59 -0
- data/lib/actn/api/version.rb +5 -0
- data/lib/actn/api.rb +16 -0
- data/test/actn/test_backend.rb +81 -0
- data/test/actn/test_client.rb +31 -0
- data/test/actn/test_connect.rb +42 -0
- data/test/actn/test_delete.rb +53 -0
- data/test/actn/test_frontend.rb +32 -0
- data/test/actn/test_query.rb +60 -0
- data/test/actn/test_upsert.rb +77 -0
- data/test/actn/test_user.rb +37 -0
- data/test/minitest_helper.rb +27 -0
- data/test/support/test.html +7 -0
- data/views/core/app.erb +1 -0
- data/views/public/connect.erb +123 -0
- metadata +258 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
3
|
+
"type": "object",
|
4
|
+
"properties": {
|
5
|
+
"apikey": {
|
6
|
+
"description": "Autogenerated API key for basic auth",
|
7
|
+
"type": "string"
|
8
|
+
},
|
9
|
+
"secret_hash": {
|
10
|
+
"description": "hashed version of api secret",
|
11
|
+
"type": "string"
|
12
|
+
},
|
13
|
+
"domain": {
|
14
|
+
"description": "Allowed domain for CORS calls",
|
15
|
+
"type": "string"
|
16
|
+
},
|
17
|
+
"acl": {
|
18
|
+
"description": "Access control list",
|
19
|
+
"type": "object",
|
20
|
+
"patternProperties": {
|
21
|
+
"[allow|disallow]": {
|
22
|
+
"type": "array",
|
23
|
+
"pattern": "/(\\*|GET|POST|PUT|PATCH|DELETE)(\\:\/.*)?/g"
|
24
|
+
}
|
25
|
+
},
|
26
|
+
"required": ["allow","disallow"]
|
27
|
+
},
|
28
|
+
"sessions": {
|
29
|
+
"description": "Active sessions belongs to client",
|
30
|
+
"type": "object",
|
31
|
+
"patternProperties": {
|
32
|
+
"/.*/": {
|
33
|
+
"type": "array",
|
34
|
+
"minItems": 2,
|
35
|
+
"maxItems": 2
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"required": ["apikey","secret_hash","domain","acl"]
|
41
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "http://json-schema.org/draft-04/schema#",
|
3
|
+
"type": "object",
|
4
|
+
"properties": {
|
5
|
+
"first_name": {
|
6
|
+
"type": "string"
|
7
|
+
},
|
8
|
+
"last_name": {
|
9
|
+
"type": "string"
|
10
|
+
},
|
11
|
+
"email": {
|
12
|
+
"type": "string",
|
13
|
+
"format": "email"
|
14
|
+
},
|
15
|
+
"hash": {
|
16
|
+
"type": "string"
|
17
|
+
},
|
18
|
+
"phone": {
|
19
|
+
"type": "string"
|
20
|
+
}
|
21
|
+
},
|
22
|
+
"required": ["first_name","last_name","email"]
|
23
|
+
}
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'actn/db/mod'
|
2
|
+
require 'actn/core_ext/string'
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
module Actn
|
6
|
+
module Api
|
7
|
+
class Client < DB::Mod
|
8
|
+
|
9
|
+
self.table = "clients"
|
10
|
+
self.schema = "core"
|
11
|
+
|
12
|
+
BCrypt::Engine.cost = 4
|
13
|
+
|
14
|
+
DEFAULT_ACL = {allow: ['*'], disallow: []}
|
15
|
+
TTL = 360
|
16
|
+
|
17
|
+
attr_accessor :secret
|
18
|
+
|
19
|
+
data_attr_accessor :apikey, :secret_hash, :domain, :sessions, :acl
|
20
|
+
|
21
|
+
validates_presence_of :apikey, :domain, :acl, :secret_hash
|
22
|
+
|
23
|
+
before_validation :set_defaults
|
24
|
+
|
25
|
+
def self.find_for_auth domain, apikey
|
26
|
+
client = self.find_by(domain: domain, apikey: apikey)
|
27
|
+
client
|
28
|
+
end
|
29
|
+
|
30
|
+
def auth_by_secret secret
|
31
|
+
self.secret == secret
|
32
|
+
end
|
33
|
+
|
34
|
+
def auth_by_session session_id
|
35
|
+
return unless client_session = self.sessions[session_id]
|
36
|
+
if BCrypt::Password.new(client_session[0]) == session_id
|
37
|
+
if Time.now.to_f - client_session[1] > TTL
|
38
|
+
invalidated = self.update(sessions: self.sessions.tap{|s| s.delete session_id })
|
39
|
+
return false
|
40
|
+
else
|
41
|
+
return true
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_session session_id
|
47
|
+
self.update( { sessions: {session_id => [BCrypt::Password.create(session_id), Time.now.to_f] }} )
|
48
|
+
end
|
49
|
+
|
50
|
+
def secret
|
51
|
+
@hash ||= BCrypt::Password.new(self.secret_hash) if self.secret_hash
|
52
|
+
end
|
53
|
+
|
54
|
+
def credentials
|
55
|
+
{'apikey' => self.apikey, 'secret' => @secret}
|
56
|
+
end
|
57
|
+
|
58
|
+
def reset_credentials!
|
59
|
+
reset_credentials
|
60
|
+
_update
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def can? resource
|
65
|
+
return if self.acl['disallow'].include?("*") || self.acl['disallow'].include?(resource)
|
66
|
+
self.acl['allow'].include?("*") || self.acl['allow'].include?(resource)
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_json options = {}
|
70
|
+
super(options.merge(methods: [:credentials], exclude: [:sessions, :secret_hash]))
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def set_defaults
|
76
|
+
self.domain = self.domain.to_domain
|
77
|
+
self.sessions ||= {}
|
78
|
+
self.acl ||= DEFAULT_ACL
|
79
|
+
reset_credentials unless self.persisted?
|
80
|
+
end
|
81
|
+
|
82
|
+
def reset_credentials
|
83
|
+
self.apikey = SecureRandom.hex
|
84
|
+
@secret = SecureRandom.hex
|
85
|
+
@hash = BCrypt::Password.create(@secret)
|
86
|
+
self.secret_hash = @hash
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'helmet'
|
2
|
+
require 'rack/csrf'
|
3
|
+
require 'actn/db'
|
4
|
+
require 'actn/api/user'
|
5
|
+
require 'actn/api/mw/auth'
|
6
|
+
require 'actn/api/mw/no_xss'
|
7
|
+
require 'actn/api/goliath/validator'
|
8
|
+
require 'actn/api/goliath/params'
|
9
|
+
|
10
|
+
module Actn
|
11
|
+
module Api
|
12
|
+
class Core < Helmet::API
|
13
|
+
|
14
|
+
OK = '{"success": true}'
|
15
|
+
|
16
|
+
def self.inherited base
|
17
|
+
|
18
|
+
base.init
|
19
|
+
|
20
|
+
super
|
21
|
+
|
22
|
+
base.use Goliath::Rack::Params
|
23
|
+
base.use Goliath::Rack::Heartbeat
|
24
|
+
|
25
|
+
base.use Mw::NoXSS
|
26
|
+
base.use Rack::Session::Cookie, secret: ENV['SECRET']
|
27
|
+
base.use Rack::Csrf, skip_if: proc { |request|
|
28
|
+
request.env.key?('HTTP_X_APIKEY') && request.env.key?('HTTP_X_SECRET')
|
29
|
+
}
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'oj'
|
2
|
+
require 'rack/utils'
|
3
|
+
|
4
|
+
module Goliath
|
5
|
+
module Rack
|
6
|
+
# URL_ENCODED = %r{^application/x-www-form-urlencoded}
|
7
|
+
# JSON_ENCODED = %r{^application/json}
|
8
|
+
|
9
|
+
# A middle ware to parse params. This will parse both the
|
10
|
+
# query string parameters and the body and place them into
|
11
|
+
# the _params_ hash of the Goliath::Env for the request.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# use Goliath::Rack::Params
|
15
|
+
#
|
16
|
+
class Params
|
17
|
+
module Parser
|
18
|
+
def retrieve_params(env)
|
19
|
+
params = env['params'] || {}
|
20
|
+
begin
|
21
|
+
params.merge!(::Rack::Utils.parse_nested_query(env['QUERY_STRING']))
|
22
|
+
if env['rack.input']
|
23
|
+
post_params = ::Rack::Utils::Multipart.parse_multipart(env)
|
24
|
+
unless post_params
|
25
|
+
body = env['rack.input'].read
|
26
|
+
env['rack.input'].rewind
|
27
|
+
|
28
|
+
unless body.empty?
|
29
|
+
post_params = case(env['CONTENT_TYPE'])
|
30
|
+
when URL_ENCODED then
|
31
|
+
::Rack::Utils.parse_nested_query(body)
|
32
|
+
when JSON_ENCODED then
|
33
|
+
json = Oj.load(body)
|
34
|
+
if json.is_a?(Hash)
|
35
|
+
json
|
36
|
+
else
|
37
|
+
{'_json' => json}
|
38
|
+
end
|
39
|
+
else
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
else
|
43
|
+
post_params = {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
params = { 'query' => params, 'data' => post_params }
|
47
|
+
end
|
48
|
+
rescue StandardError => e
|
49
|
+
raise Goliath::Validation::BadRequestError, "Invalid parameters: #{e.class.to_s}"
|
50
|
+
end
|
51
|
+
params
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
include Goliath::Rack::Validator
|
56
|
+
include Parser
|
57
|
+
|
58
|
+
def initialize(app)
|
59
|
+
@app = app
|
60
|
+
end
|
61
|
+
|
62
|
+
def call(env)
|
63
|
+
Goliath::Rack::Validator.safely(env) do
|
64
|
+
env['params'] = retrieve_params(env)
|
65
|
+
@app.call(env)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'oj'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
module Validator
|
6
|
+
module_function
|
7
|
+
ERROR = 'error'
|
8
|
+
|
9
|
+
# @param status_code [Integer] HTTP status code for this error.
|
10
|
+
# @param msg [String] message to inject into the response body.
|
11
|
+
# @param headers [Hash] Response headers to preserve in an error response;
|
12
|
+
# (the Content-Length header, if any, is removed)
|
13
|
+
def validation_error(status_code, msg, headers={})
|
14
|
+
headers.delete('Content-Length')
|
15
|
+
[status_code, headers, Oj.dump({ERROR => msg})]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Execute a block of code safely.
|
19
|
+
#
|
20
|
+
# If the block raises any exception that derives from
|
21
|
+
# Goliath::Validation::Error (see specifically those in
|
22
|
+
# goliath/validation/standard_http_errors.rb), it will be turned into the
|
23
|
+
# corresponding 4xx response with a corresponding message.
|
24
|
+
#
|
25
|
+
# If the block raises any other kind of error, we log it and return a
|
26
|
+
# less-communicative 500 response.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# # will convert the ForbiddenError exception into a 403 response
|
30
|
+
# # and an uncaught error in do_something_risky! into a 500 response
|
31
|
+
# safely(env, headers) do
|
32
|
+
# raise ForbiddenError unless account_info['valid'] == true
|
33
|
+
# do_something_risky!
|
34
|
+
# [status, headers, body]
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# @param env [Goliath::Env] The current request env
|
39
|
+
# @param headers [Hash] Response headers to preserve in an error response
|
40
|
+
#
|
41
|
+
def safely(env, headers={})
|
42
|
+
begin
|
43
|
+
yield
|
44
|
+
rescue Goliath::Validation::Error => e
|
45
|
+
validation_error(e.status_code, e.message, headers)
|
46
|
+
rescue Exception => e
|
47
|
+
env.logger.error(e.message)
|
48
|
+
env.logger.error(e.backtrace.join("\n"))
|
49
|
+
validation_error(500, e.message, headers)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'goliath/rack'
|
2
|
+
require 'goliath/validation'
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
##
|
6
|
+
# Bync.rb
|
7
|
+
# Bouncer of our midningt api club
|
8
|
+
|
9
|
+
module Actn
|
10
|
+
module Api
|
11
|
+
module Mw
|
12
|
+
class Auth
|
13
|
+
|
14
|
+
include Goliath::Rack::BarrierAroundware
|
15
|
+
include Goliath::Validation
|
16
|
+
|
17
|
+
class MissingApikeyError < BadRequestError ; end
|
18
|
+
class InvalidCredentialsError < UnauthorizedError ; end
|
19
|
+
|
20
|
+
attr_accessor :client, :opts
|
21
|
+
|
22
|
+
def initialize(env, opts = {})
|
23
|
+
self.opts = opts
|
24
|
+
super(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def pre_process
|
29
|
+
|
30
|
+
unless excluded?
|
31
|
+
|
32
|
+
validate_apikey!
|
33
|
+
|
34
|
+
# On non-GET non-HEAD requests, we have to check auth now.
|
35
|
+
unless lazy_authorization?
|
36
|
+
perform # yield execution until user_info has arrived
|
37
|
+
authorize_client!
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
return Goliath::Connection::AsyncResponse
|
43
|
+
end
|
44
|
+
|
45
|
+
def post_process
|
46
|
+
|
47
|
+
unless excluded?
|
48
|
+
|
49
|
+
# We have to check auth now, we skipped it before
|
50
|
+
if lazy_authorization?
|
51
|
+
validate_client!
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
[status, headers, body]
|
57
|
+
end
|
58
|
+
|
59
|
+
def lazy_authorization?
|
60
|
+
(env['REQUEST_METHOD'] == 'GET') || (env['REQUEST_METHOD'] == 'HEAD')
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_apikey!
|
64
|
+
return true if with_session? && current_user_uuid
|
65
|
+
raise MissingApikeyError.new("Missing Api Key") if apikey.to_s.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
def validate_client!
|
69
|
+
return true if with_session? && current_user_uuid
|
70
|
+
raise Goliath::Validation::UnauthorizedError unless client_valid?
|
71
|
+
end
|
72
|
+
|
73
|
+
def authorize_client!
|
74
|
+
return true if with_session? && current_user_uuid
|
75
|
+
unless client_valid? && client_authorized?
|
76
|
+
raise InvalidCredentialsError.new("Invalid Credentials")
|
77
|
+
end
|
78
|
+
env['rack.session'][:user_uuid] = self.client.uuid
|
79
|
+
end
|
80
|
+
|
81
|
+
def apikey
|
82
|
+
env['HTTP_X_APIKEY']
|
83
|
+
end
|
84
|
+
|
85
|
+
def secret
|
86
|
+
env['HTTP_X_SECRET']
|
87
|
+
end
|
88
|
+
|
89
|
+
def client_valid?
|
90
|
+
self.client = Client.find_for_auth(host, apikey)
|
91
|
+
end
|
92
|
+
|
93
|
+
def client_authorized?
|
94
|
+
return unless self.client
|
95
|
+
(
|
96
|
+
self.secret.nil? ?
|
97
|
+
self.client.auth_by_session(env['rack.session'].id) :
|
98
|
+
self.client.auth_by_secret(self.secret)
|
99
|
+
) && self.client.can?("#{env['REQUEST_METHOD']}:#{env['REQUEST_PATH']}")
|
100
|
+
end
|
101
|
+
|
102
|
+
def host
|
103
|
+
(env['HTTP_ORIGIN'] || env['HTTP_HOST']).to_domain
|
104
|
+
end
|
105
|
+
|
106
|
+
def excluded?
|
107
|
+
opts[:exclude].nil? ? false : (env['REQUEST_PATH'] =~ opts[:exclude])
|
108
|
+
end
|
109
|
+
|
110
|
+
def with_session?
|
111
|
+
opts[:with_session]
|
112
|
+
end
|
113
|
+
|
114
|
+
def current_user_uuid
|
115
|
+
env['rack.session'][:user_uuid]
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'goliath/rack'
|
2
|
+
require 'rack/csrf'
|
3
|
+
|
4
|
+
##
|
5
|
+
# Cors.rb
|
6
|
+
# Cross domain middle ware
|
7
|
+
|
8
|
+
|
9
|
+
module Actn
|
10
|
+
module Api
|
11
|
+
module Mw
|
12
|
+
class Cors
|
13
|
+
|
14
|
+
include Goliath::Rack::AsyncMiddleware
|
15
|
+
|
16
|
+
def initialize(app)
|
17
|
+
@app = app
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
if env['REQUEST_METHOD'] == 'OPTIONS'
|
22
|
+
[200, cors_headers(env,false), []]
|
23
|
+
else
|
24
|
+
super(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def post_process(env, status, headers, body)
|
29
|
+
headers = cors_headers(env).merge(headers)
|
30
|
+
[status, headers, body]
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_accessor :options
|
36
|
+
|
37
|
+
def cors_headers env, csrf = true
|
38
|
+
headers = {}
|
39
|
+
headers['Access-Control-Allow-Credentials'] = 'true'
|
40
|
+
headers['Access-Control-Allow-Origin'] = env['HTTP_ORIGIN']
|
41
|
+
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, PATCH, DELETE, OPTIONS'
|
42
|
+
headers['Access-Control-Allow-Headers'] = '*, X-Requested-With, X-Prototype-Version, X-CSRF-Token, Authorization, Origin, Accept, Content-Type, Referer'
|
43
|
+
headers['Access-Control-Expose-Headers'] = 'X_CSRF_TOKEN, X_APIKEY'
|
44
|
+
headers['Access-Control-Max-Age'] = "#{Client::TTL}"
|
45
|
+
headers['P3P'] = 'CP="IDC DSP COR CURa ADMa OUR IND PHY ONL COM STA"'
|
46
|
+
|
47
|
+
headers['X_CSRF_TOKEN'] = Rack::Csrf.token(env) if csrf
|
48
|
+
|
49
|
+
client_headers_to_approve = env['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'].to_s.gsub(/[^\w\-\,]+/,'')
|
50
|
+
headers['Access-Control-Allow-Headers'] += ",#{client_headers_to_approve}" if not client_headers_to_approve.empty?
|
51
|
+
|
52
|
+
headers
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'goliath/rack'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Sec.rb
|
5
|
+
# Security comes first
|
6
|
+
|
7
|
+
module Actn
|
8
|
+
module Api
|
9
|
+
module Mw
|
10
|
+
class NoXSS
|
11
|
+
|
12
|
+
include Goliath::Rack::AsyncMiddleware
|
13
|
+
|
14
|
+
HEADERS = {
|
15
|
+
'X-Frame-Options' => 'SAMEORIGIN',
|
16
|
+
'X-XSS-Protection' => '1; mode=block',
|
17
|
+
'X-Content-Type-Options' => 'nosniff'
|
18
|
+
}
|
19
|
+
|
20
|
+
def post_process(env, status, headers, body)
|
21
|
+
headers.update HEADERS
|
22
|
+
[status, headers, body]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'goliath'
|
2
|
+
require 'actn/db/set'
|
3
|
+
require 'actn/api/client'
|
4
|
+
require 'actn/api/mw/auth'
|
5
|
+
require 'actn/api/mw/cors'
|
6
|
+
require 'actn/api/mw/no_xss'
|
7
|
+
require 'actn/api/goliath/validator'
|
8
|
+
require 'actn/api/goliath/params'
|
9
|
+
|
10
|
+
module Actn
|
11
|
+
module Api
|
12
|
+
class Public < Goliath::API
|
13
|
+
|
14
|
+
CT_JS = { 'Content-Type' => 'application/javascript' }
|
15
|
+
CT_JSON = {'Content-Type' => 'application/json'}
|
16
|
+
|
17
|
+
|
18
|
+
def self.inherited base
|
19
|
+
|
20
|
+
super
|
21
|
+
|
22
|
+
base.use Goliath::Rack::Params
|
23
|
+
base.use Goliath::Rack::Heartbeat
|
24
|
+
|
25
|
+
base.use Mw::NoXSS
|
26
|
+
base.use Rack::Session::Cookie, secret: ENV['SECRET']
|
27
|
+
base.use Rack::Csrf, skip: ['OPTIONS:/.*'], skip_if: proc { |r| ENV['RACK_ENV'] == "test" }
|
28
|
+
base.use Mw::Cors
|
29
|
+
base.use Goliath::Rack::BarrierAroundwareFactory, Mw::Auth, exclude: /^\/connect$/
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def process table, path
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
def response env
|
42
|
+
|
43
|
+
path = env['REQUEST_PATH'] || "/"
|
44
|
+
|
45
|
+
unless table = path[1..-1].split("/").first
|
46
|
+
raise Goliath::Validation::Error.new(400, "model identifier missing")
|
47
|
+
end
|
48
|
+
|
49
|
+
begin
|
50
|
+
json = process(table,path)
|
51
|
+
rescue PG::InternalError => e
|
52
|
+
if e.message =~ /does not exist/
|
53
|
+
raise Goliath::Validation::NotFoundError.new("resource not found")
|
54
|
+
else
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
status = json =~ /errors/ ? 406 : 200
|
60
|
+
|
61
|
+
[status, CT_JSON, json]
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def limit
|
68
|
+
query['limit'] || 50
|
69
|
+
end
|
70
|
+
|
71
|
+
def page
|
72
|
+
((query['page'] || 1).to_i - 1)
|
73
|
+
end
|
74
|
+
|
75
|
+
def offset
|
76
|
+
limit * page
|
77
|
+
end
|
78
|
+
|
79
|
+
def query
|
80
|
+
params['query']
|
81
|
+
end
|
82
|
+
|
83
|
+
def data
|
84
|
+
params['data']
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'actn/db/mod'
|
2
|
+
require 'actn/core_ext/string'
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
module Actn
|
6
|
+
|
7
|
+
module Api
|
8
|
+
|
9
|
+
class User < DB::Mod
|
10
|
+
|
11
|
+
BCrypt::Engine.cost = 4
|
12
|
+
|
13
|
+
self.schema = "core"
|
14
|
+
self.table = "users"
|
15
|
+
|
16
|
+
def self.find_for_auth params
|
17
|
+
return unless user = self.find_by('email' => params['email'])
|
18
|
+
return unless user.password == params['password']
|
19
|
+
user.uuid
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :password, :password_confirmation
|
23
|
+
|
24
|
+
data_attr_accessor :first_name, :last_name, :email, :hash, :phone
|
25
|
+
|
26
|
+
validates_presence_of :first_name, :last_name, :email
|
27
|
+
validates_presence_of :password, :password_confirmation, unless: 'persisted?'
|
28
|
+
validates_confirmation_of :password, unless: 'persisted?'
|
29
|
+
validate :validate_unique_email, unless: 'persisted?'
|
30
|
+
|
31
|
+
before_create :set_password
|
32
|
+
|
33
|
+
def password
|
34
|
+
if self.hash
|
35
|
+
@password ||= BCrypt::Password.new(self.hash)
|
36
|
+
else
|
37
|
+
@password
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_json options = {}
|
42
|
+
super(options.merge(:exclude [:hash]))
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def set_password
|
48
|
+
@password = BCrypt::Password.create(self.password)
|
49
|
+
self.hash = @password
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def validate_unique_email
|
54
|
+
errors.add(:email, "has already been taken") if self.class.find_by('email' => self.email)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/actn/api.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "actn/paths"
|
2
|
+
require "actn/db"
|
3
|
+
require "actn/api/version"
|
4
|
+
|
5
|
+
module Actn
|
6
|
+
module Api
|
7
|
+
include Paths
|
8
|
+
|
9
|
+
def self.gem_root
|
10
|
+
@@gem_root ||= File.expand_path('../../../', __FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Actn::DB.paths << Actn::Api.gem_root
|