actn-api 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|