dash_api 0.0.16 → 0.0.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +137 -36
- data/app/controllers/concerns/dash_api/api_exception.rb +26 -0
- data/app/controllers/concerns/dash_api/auth.rb +51 -0
- data/app/controllers/dash_api/api_controller.rb +57 -40
- data/app/controllers/dash_api/application_controller.rb +3 -38
- data/app/controllers/dash_api/schema_controller.rb +0 -2
- data/app/models/concerns/dash_api/dash_model.rb +35 -162
- data/app/models/dash_api/dash_table.rb +1 -1
- data/app/services/dash_api/json_web_token.rb +26 -6
- data/app/services/dash_api/query.rb +16 -13
- data/app/services/dash_api/serializer.rb +19 -0
- data/lib/dash_api/version.rb +1 -1
- data/lib/dash_api.rb +2 -3
- metadata +38 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab623028d1c6a931fef116ca98b2b990782464c78648005accd420039dcfd2be
|
4
|
+
data.tar.gz: bcbb8bb65b2805bfe936a232ee9cb5ed1863b9d7a44582d26c719336693a4a88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8db9bab64403ea901c0b2058df133a708d7af4189269bdb3b526a3b5871cda47a5c50a82a6c2f6f2020a850eb75557cf07c3153f3019032fa6f2108fed784cf0
|
7
|
+
data.tar.gz: 210e81c2b3f795d762a162348bc404d2cc1dcde299023b0f1fe983f1ca6fd3b2a237ab043e5ada224ce4ef1cf9f61cd1d3ed353d139cfba2392f4f020c11b6c9
|
data/README.md
CHANGED
@@ -16,6 +16,8 @@ DashAPI supports several features out of the box with little or no configuration
|
|
16
16
|
- Associations
|
17
17
|
- Pagination
|
18
18
|
- JWT token authorization
|
19
|
+
- Firebase authorization
|
20
|
+
- Role-based access control
|
19
21
|
|
20
22
|
DashAPI is designed to help rapidly build fully functional, scalable and secure applications
|
21
23
|
by automatically generating REST APIs using any Postgres database. DashAPI also supports
|
@@ -39,9 +41,9 @@ Mount the Dash API by updating your `routes.rb` file:
|
|
39
41
|
mount DashApi::Engine, at: "/dash/api"
|
40
42
|
```
|
41
43
|
|
42
|
-
|
44
|
+
Install the pundit policy
|
43
45
|
```bash
|
44
|
-
|
46
|
+
rails g pundit:install
|
45
47
|
```
|
46
48
|
|
47
49
|
You can also configure DashApi with an initializer by creating a file at `config/initializers/dash_api.rb`.
|
@@ -50,13 +52,11 @@ You can also configure DashApi with an initializer by creating a file at `config
|
|
50
52
|
# Place file at config/initializers/dash_api.rb
|
51
53
|
|
52
54
|
DashApi.tap |config|
|
53
|
-
config.enable_auth = ENV['DASH_ENABLE_AUTH'] === true
|
54
|
-
config.api_token = ENV['DASH_API_TOKEN']
|
55
55
|
config.jwt_secret = ENV['DASH_JWT_SECRET']
|
56
|
-
config.exclude_fields = ENV['DASH_API_TOKEN'].split(" ") || []
|
57
|
-
config.exclude_tables = ENV['DASH_API_TOKEN'].split(" ") || []
|
58
56
|
end
|
59
57
|
```
|
58
|
+
|
59
|
+
|
60
60
|
The DashAPI is now ready. You can view all tables at:
|
61
61
|
`/dash/api`
|
62
62
|
|
@@ -126,6 +126,9 @@ GET /dash/api/books?filters=id:lt:10,published:eq:true
|
|
126
126
|
```
|
127
127
|
|
128
128
|
The currently supported operators are:
|
129
|
+
|
130
|
+
|
131
|
+
|
129
132
|
```
|
130
133
|
eq - Equals
|
131
134
|
neq - Not equals
|
@@ -296,46 +299,55 @@ Body
|
|
296
299
|
}
|
297
300
|
```
|
298
301
|
|
299
|
-
###
|
302
|
+
### JWT Token Authorization
|
300
303
|
|
301
|
-
|
304
|
+
The recommended way to secure your API is to use a JWT token. To enable a JWT token, you must first
|
305
|
+
specify the JWT secret key in your configuration at `config/initializers/dash_api.rb`
|
302
306
|
|
303
|
-
|
307
|
+
Dash API is designed to work alongside an existing API or an additional server which handles authentication. This is accomplished by using a shared JWT secret that is to decode the JWT token.
|
304
308
|
|
309
|
+
The JWT decoded object should be a json object and is expected to have a "role" field and a
|
310
|
+
corresponding "id" field to identify the ID of the user.
|
305
311
|
|
306
312
|
```
|
307
313
|
# /config/initializers/dash_api.rb
|
308
314
|
|
309
315
|
DashApi.tap do |config|
|
310
|
-
config.
|
311
|
-
config.api_token = ENV['DASH_API_TOKEN']
|
316
|
+
config.jwt_secret = ENV['DASH_JWT_SECRET']
|
312
317
|
...
|
313
318
|
end
|
314
319
|
```
|
315
320
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
### JWT Token Authentication
|
321
|
+
### Firebase Authorization
|
320
322
|
|
321
|
-
|
322
|
-
specify the JWT secret key in your `config/initializers/dash_api.rb`
|
323
|
-
|
324
|
-
Note that if your Dash API works alongside an existing API or an additional server which handles authentication, you can use a shared JWT secret that is used by both services to decode the JWT token. The JWT tokenization
|
325
|
-
uses the RS
|
323
|
+
DashAPI also supports handling [Firebase](https://firebase.google.com/) Authentication. When you sign in with a supported Firebase method, you will receive an `idToken` which is an encoded JWT token. You may use this token in your requests identically as you would a standard JWT token to authorize requests.
|
326
324
|
|
325
|
+
To add Firebase support, add your Firebase Web API Key located under Firebase > Project > Settings to your Dash API configuration.
|
327
326
|
|
328
327
|
```
|
329
328
|
# /config/initializers/dash_api.rb
|
330
329
|
|
331
330
|
DashApi.tap do |config|
|
332
|
-
config.
|
333
|
-
config.jwt_secret = ENV['DASH_JWT_SECRET']
|
331
|
+
config.firebase_web_key = ENV['FIREBASE_WEB_KEY']
|
334
332
|
...
|
335
333
|
end
|
336
334
|
```
|
337
335
|
|
338
|
-
|
336
|
+
Note that since Firebase does not by default assign a `role` to the JWT payload, the DashAPI will inject the `role` key with value `user` to make it easier to manage your access policies. It will also inject a `provider` key with value `firebase` to more easily identify firebase requests in your Pundit policies.
|
337
|
+
|
338
|
+
For documentation on how to sign in users with Firebase, please check out the official [Firebase Authentication documentation](https://firebase.google.com/docs/auth).
|
339
|
+
|
340
|
+
### API Authentication
|
341
|
+
|
342
|
+
To authenticate your requests, pass the encoded JWT token in your authorization headers:
|
343
|
+
```
|
344
|
+
Authorization: 'Bearer <JWT_TOKEN>'
|
345
|
+
```
|
346
|
+
|
347
|
+
You can also pass the token as a url paramter with every request:
|
348
|
+
```
|
349
|
+
/dash/api/...?token=<JWT_TOKEN>
|
350
|
+
```
|
339
351
|
|
340
352
|
The JWT token will also inspect for the `exp` key and if present will only allow requests with valid
|
341
353
|
expiration timestamps. For security purposes it's recommended that you encode your JWT tokens with an exp
|
@@ -343,43 +355,132 @@ timestamp.
|
|
343
355
|
|
344
356
|
To setup and test JWT tokens, we recommend you explore [jwt.io](https://jwt.io).
|
345
357
|
|
346
|
-
###
|
358
|
+
### Authorization (Pundit)
|
347
359
|
|
348
|
-
|
349
|
-
|
360
|
+
DashAPI uses the popular Ruby on Rails [Pundet](https://github.com/varvet/pundit) gem to manage
|
361
|
+
the authorization policies. Using pundit, you can restrict access to any table or method within a table
|
362
|
+
according to "policies" that you define. These policies map to the User object that is passed from the JWT token.
|
363
|
+
|
364
|
+
When you first installed DashAPI, you run the pundit installation generator which creates an `ApplicationPolicy` file in `app/policies.` `ApplicationPolicy` defines all the default policies inherited by all tables in the DashAPI.
|
365
|
+
|
366
|
+
The benefit of using Pundit is that you can easily create an acesss policy or search scope by creating a policy for each table in your database that differs from the default policy. To do this, simple create a ruby class that matches the name of the table class followed by the term "Policy." For example, if you want to create a policy for your `orders` table then you can create a ruby class as follows
|
350
367
|
|
351
|
-
You can pass the token as a url paramter:
|
352
368
|
```
|
353
|
-
|
369
|
+
# Place this file in /app/policies/order_policy.rb
|
370
|
+
|
371
|
+
def OrderPolicy < ApplicationPolicy
|
372
|
+
...
|
373
|
+
end
|
354
374
|
```
|
355
375
|
|
356
|
-
|
376
|
+
You can then specify the polify for each operation from the API. First, below is a sample policy class used by Pundit without any restrictions in place:
|
377
|
+
|
357
378
|
```
|
358
|
-
|
379
|
+
class OrderPolicy < ApplicationPolicy
|
380
|
+
|
381
|
+
def index?
|
382
|
+
true
|
383
|
+
end
|
384
|
+
|
385
|
+
def show?
|
386
|
+
true
|
387
|
+
end
|
388
|
+
|
389
|
+
def update?
|
390
|
+
true
|
391
|
+
end
|
392
|
+
|
393
|
+
def destroy?
|
394
|
+
true
|
395
|
+
end
|
396
|
+
|
397
|
+
class Scope
|
398
|
+
def initialize(user, scope)
|
399
|
+
@user = user
|
400
|
+
@scope = scope
|
401
|
+
end
|
402
|
+
|
403
|
+
def resolve
|
404
|
+
scope.all
|
405
|
+
end
|
406
|
+
|
407
|
+
private
|
408
|
+
|
409
|
+
attr_reader :user, :scope
|
410
|
+
end
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
### Authorization for scope queries
|
415
|
+
|
416
|
+
One common scenario is to restrict access to all `Orders` that only belong to the current user, unless ther user has role `admin` any have access to all `Orders.` You can easily achieve this by specifying the Scope class `resolve` method:
|
417
|
+
|
359
418
|
```
|
419
|
+
def resolve
|
420
|
+
if @user.role === 'admin'
|
421
|
+
scope.all
|
422
|
+
else
|
423
|
+
scope.where(user_id: @user.id)
|
424
|
+
end
|
425
|
+
end
|
426
|
+
```
|
427
|
+
|
428
|
+
This will restrict all order results to those that only belong to the current user. Again, the user is the JSON payload that is decoded using the JWT token. For example, the token you encode and decode should have at the very least a unique identifier such as `id` or `uid` and a `role` field:
|
429
|
+
|
430
|
+
{
|
431
|
+
id: 1,
|
432
|
+
first_name: 'John',
|
433
|
+
last_name: 'Doe',
|
434
|
+
role: 'admin'
|
435
|
+
}
|
436
|
+
|
437
|
+
When this JWT token is passed to Pundit using DashAPI, it will be accessible as `@user` and you can reference any attribute on user such as `@user.id` and `@user.role` within the Pundit policy class.
|
360
438
|
|
361
439
|
|
362
|
-
###
|
440
|
+
### Authorization for CRUD operations
|
363
441
|
|
364
|
-
|
442
|
+
You can also override ride any specific CRUD operation by specifying the policy for that operation. This will allow you to provide access control that changes for `admins` and `users`.
|
365
443
|
|
366
|
-
|
444
|
+
As an example, lets only allow users to be able to delete an order if the order status is a `draft` order, and not an order that as been `paid.`
|
445
|
+
|
446
|
+
```
|
447
|
+
def destroy?
|
448
|
+
if @user.role === 'admin'
|
449
|
+
true
|
450
|
+
else
|
451
|
+
@user.id === @record.user_id && @record.status === 'draft'
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
Pundit provides a simple yet powerful way to manage the access control policies of the DashAPI for any table.
|
457
|
+
|
458
|
+
|
459
|
+
### Serialization
|
460
|
+
|
461
|
+
DashAPI will serialize all data from the API using the `as_json` method on the Active Record object. You can exclude sensitive attributes from being serialized during `as_json` by specifying which attributes to ignore in `DashAPi.exclude_attributes` using space delimited attribute names.
|
367
462
|
|
368
463
|
```
|
369
464
|
# /config/initializers/dash_api.rb
|
370
465
|
DashApi.tap do |config|
|
371
466
|
...
|
372
|
-
config.
|
467
|
+
config.exclude_attributes = ENV['DASH_EXCLUDE_ATTRIBUTES'].split(' ') || []
|
373
468
|
...
|
374
469
|
end
|
375
470
|
```
|
376
471
|
|
377
472
|
Example:
|
473
|
+
|
378
474
|
```
|
379
|
-
config.
|
475
|
+
# /config/initializers/dash_api.rb
|
476
|
+
DashApi.tap do |config|
|
477
|
+
...
|
478
|
+
config.exclude_attributes = "encrypted_password hashed_password"
|
479
|
+
...
|
480
|
+
end
|
380
481
|
```
|
381
482
|
|
382
|
-
You can also exclude tables from the API using the exclude_tables configuration:
|
483
|
+
You can also exclude tables entirely from the API using the exclude_tables configuration by using space delimited table names:
|
383
484
|
|
384
485
|
```
|
385
486
|
# /config/initializers/dash_api.rb
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module DashApi
|
2
|
+
module ApiException
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include Pundit
|
7
|
+
|
8
|
+
rescue_from Exception, with: :unprocessable_entity
|
9
|
+
rescue_from StandardError, with: :unprocessable_entity
|
10
|
+
rescue_from ActiveRecord::RecordNotFound, with: :unprocessable_entity
|
11
|
+
rescue_from ActiveRecord::ActiveRecordError, with: :unprocessable_entity
|
12
|
+
rescue_from Pundit::NotAuthorizedError, with: :unauthorized
|
13
|
+
|
14
|
+
|
15
|
+
def unprocessable_entity(e)
|
16
|
+
render json: { error: e }, status: :unprocessable_entity
|
17
|
+
end
|
18
|
+
|
19
|
+
def unauthorized(e)
|
20
|
+
render json: { error: "You are not authorized to perform this action." }, status: :unprocessable_entity
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module DashApi
|
2
|
+
module Auth
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def current_user
|
10
|
+
unless auth_token.blank?
|
11
|
+
jwt_payload = jwt_token
|
12
|
+
else
|
13
|
+
jwt_payload = {role: "guest"}
|
14
|
+
end
|
15
|
+
HashWithIndifferentAccess.new(jwt_payload)
|
16
|
+
end
|
17
|
+
|
18
|
+
def jwt_token
|
19
|
+
case jwt_token_provider
|
20
|
+
when "firebase"
|
21
|
+
DashApi::JsonWebToken.decode_firebase(auth_token)
|
22
|
+
when "jwt"
|
23
|
+
DashApi::JsonWebToken.decode(auth_token)
|
24
|
+
else
|
25
|
+
raise "Unknown JWT token provider"
|
26
|
+
end
|
27
|
+
rescue JWT::ExpiredSignature
|
28
|
+
raise "JWT token has expired"
|
29
|
+
rescue JWT::VerificationError, JWT::DecodeError
|
30
|
+
raise "Invalid JWT token"
|
31
|
+
end
|
32
|
+
|
33
|
+
def jwt_token_provider
|
34
|
+
jwt = DashApi::JsonWebToken.decode_unverified(auth_token)
|
35
|
+
jwt[:firebase].present? ? "firebase" : "jwt"
|
36
|
+
end
|
37
|
+
|
38
|
+
def auth_token
|
39
|
+
http_token || params['token']
|
40
|
+
end
|
41
|
+
|
42
|
+
def http_token
|
43
|
+
if request.headers['Authorization'].present?
|
44
|
+
request.headers['Authorization'].split(' ').last
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -1,88 +1,105 @@
|
|
1
1
|
module DashApi
|
2
|
-
class ApiController < ApplicationController
|
2
|
+
class ApiController < ApplicationController
|
3
3
|
|
4
4
|
skip_before_action :verify_authenticity_token
|
5
5
|
|
6
|
-
before_action :authenticate_request!
|
7
|
-
before_action :load_table
|
8
6
|
before_action :parse_query_params
|
7
|
+
before_action :load_dash_table
|
9
8
|
|
10
|
-
def index
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
)
|
9
|
+
def index
|
10
|
+
resources = dash_scope
|
11
|
+
|
12
|
+
authorize resources, :index?
|
13
|
+
|
14
|
+
@filters.each{|filter| resources = resources.where(filter) }
|
15
|
+
resources = resources.pg_search(@keywords) if @keywords.present?
|
16
|
+
resources = resources.order(@order) if @order.present?
|
17
|
+
resources = resources.select(@select) if @select.present?
|
18
|
+
resources = resources.page(@page).per(@per_page)
|
21
19
|
|
22
20
|
render json: {
|
23
|
-
data:
|
24
|
-
meta:
|
21
|
+
data: DashApi::Serializer.to_json(resources, includes: @includes),
|
22
|
+
meta: {
|
23
|
+
page: @page,
|
24
|
+
per_page: @per_page,
|
25
|
+
total_count: resources.total_count
|
26
|
+
}
|
25
27
|
}
|
26
28
|
end
|
27
29
|
|
28
30
|
def show
|
29
|
-
resource =
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
)
|
35
|
-
render json: { data: @dash_table.serialize[0] }
|
31
|
+
resource = dash_scope.find(params[:id])
|
32
|
+
authorize resource, :show?
|
33
|
+
render json: {
|
34
|
+
data: DashApi::Serializer.to_json(resource, includes: @includes)
|
35
|
+
}
|
36
36
|
end
|
37
37
|
|
38
38
|
def create
|
39
|
-
resource =
|
40
|
-
|
39
|
+
resource = dash_scope.create!(dash_params)
|
40
|
+
authorize resource, :create?
|
41
|
+
render json: {
|
42
|
+
data: DashApi::Serializer.to_json(resource)
|
43
|
+
}
|
41
44
|
end
|
42
45
|
|
43
46
|
def update
|
44
|
-
resource =
|
47
|
+
resource = dash_scope.find(params[:id])
|
48
|
+
authorize resource, :update?
|
45
49
|
if resource.update(dash_params)
|
46
|
-
render json: {
|
50
|
+
render json: {
|
51
|
+
data: DashApi::Serializer.to_json(resource)
|
52
|
+
}
|
47
53
|
else
|
48
54
|
render json: { error: resource.errors.full_messages }, status: 422
|
49
55
|
end
|
50
56
|
end
|
51
57
|
|
52
58
|
def destroy
|
53
|
-
resource =
|
59
|
+
resource = dash_scope.find(params[:id])
|
60
|
+
authorize resource, :destroy?
|
54
61
|
resource.destroy
|
55
|
-
render json: { data: resource }
|
62
|
+
render json: { data: DashApi::Serializer.to_json(resource) }
|
56
63
|
end
|
57
64
|
|
58
65
|
def update_many
|
59
|
-
resources =
|
66
|
+
resources = dash_scope.where(id: params[:ids])
|
67
|
+
authorize resources, :update?
|
60
68
|
resources.update(dash_params)
|
61
|
-
render json: { data: resources }
|
69
|
+
render json: { data: DashApi::Serializer.to_json(resources) }
|
62
70
|
end
|
63
71
|
|
64
72
|
def delete_many
|
65
|
-
resources =
|
73
|
+
resources = dash_scope.where(id: params[:ids])
|
74
|
+
authorize resources, :destroy?
|
66
75
|
resources.destroy_all
|
67
|
-
render json: { data: resources }
|
76
|
+
render json: { data: DashApi::Serializer.to_json(resources) }
|
68
77
|
end
|
69
78
|
|
70
79
|
private
|
71
80
|
|
72
81
|
def parse_query_params
|
73
|
-
|
82
|
+
query = DashApi::Query.parse(params)
|
83
|
+
@keywords = query[:keywords]
|
84
|
+
@page =query[:page]
|
85
|
+
@per_page = query[:per_page]
|
86
|
+
@order = query[:order]
|
87
|
+
@filters = query[:filters]
|
88
|
+
@select = query[:select_fields]
|
89
|
+
@includes = query[:associations]
|
74
90
|
end
|
75
91
|
|
76
|
-
def
|
92
|
+
def load_dash_table
|
77
93
|
raise "This resource is excluded." if DashApi.exclude_tables.include?(params[:table_name])
|
78
|
-
@dash_table = DashTable.modelize(params[:table_name])
|
79
|
-
|
94
|
+
@dash_table = DashTable.modelize(params[:table_name], includes: @includes)
|
95
|
+
end
|
96
|
+
|
97
|
+
def dash_scope
|
98
|
+
policy_scope(@dash_table)
|
80
99
|
end
|
81
100
|
|
82
101
|
def dash_params
|
83
|
-
params
|
84
|
-
.require(params[:table_name])
|
85
|
-
.permit!
|
102
|
+
params.require(params[:table_name]).permit!
|
86
103
|
end
|
87
104
|
|
88
105
|
end
|
@@ -1,42 +1,7 @@
|
|
1
1
|
module DashApi
|
2
|
-
class ApplicationController < ActionController::Base
|
3
|
-
|
4
|
-
|
5
|
-
rescue_from StandardError, with: :unprocessable_entity
|
6
|
-
rescue_from ActiveRecord::RecordNotFound, with: :unprocessable_entity
|
7
|
-
rescue_from ActiveRecord::ActiveRecordError, with: :unprocessable_entity
|
8
|
-
|
9
|
-
|
10
|
-
def authenticate_request!
|
11
|
-
if DashApi.enable_auth === true
|
12
|
-
return true if auth_token === DashApi.api_token && !DashApi.api_token.blank?
|
13
|
-
jwt_token
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def jwt_token
|
20
|
-
DashApi::JsonWebToken.decode(auth_token)
|
21
|
-
rescue JWT::ExpiredSignature
|
22
|
-
raise "JWT token has expired"
|
23
|
-
rescue JWT::VerificationError, JWT::DecodeError
|
24
|
-
raise "Invalid JWT token"
|
25
|
-
end
|
26
|
-
|
27
|
-
def auth_token
|
28
|
-
http_token || params['token']
|
29
|
-
end
|
30
|
-
|
31
|
-
def http_token
|
32
|
-
if request.headers['Authorization'].present?
|
33
|
-
request.headers['Authorization'].split(' ').last
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def unprocessable_entity(e)
|
38
|
-
render json: { error: e }, status: :unprocessable_entity
|
39
|
-
end
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
include ApiException
|
4
|
+
include Auth
|
40
5
|
|
41
6
|
end
|
42
7
|
end
|
@@ -2,202 +2,75 @@ module DashApi
|
|
2
2
|
module DashModel
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
class_methods do
|
5
|
+
class_methods do
|
6
6
|
|
7
|
-
|
7
|
+
def modelize(table_name, includes: nil)
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
page: 1,
|
17
|
-
per_page: 20
|
18
|
-
)
|
19
|
-
|
20
|
-
@current_scope = self.all
|
21
|
-
|
22
|
-
filter(filters: filters)
|
23
|
-
|
24
|
-
sort(sort_by: sort_by, sort_direction: sort_direction)
|
25
|
-
|
26
|
-
full_text_search(keywords: keywords)
|
27
|
-
|
28
|
-
join_associations(associations: associations)
|
29
|
-
|
30
|
-
select_fields(fields: select_fields)
|
31
|
-
|
32
|
-
paginate(page: page, per_page: per_page)
|
33
|
-
|
34
|
-
@current_scope
|
35
|
-
end
|
36
|
-
|
37
|
-
# Filtering
|
38
|
-
# Filtering follows the URL pattern <field>.<attribute>=<operator>.<value>
|
39
|
-
# Filters can also be chained to AND filter queries together
|
40
|
-
#
|
41
|
-
# Example: /books?books.id=lte.10 returns all books with id <= 10
|
42
|
-
|
43
|
-
def filter(filters: [])
|
44
|
-
filters.each do |filter|
|
45
|
-
@current_scope = @current_scope.where(["#{filter[:field]} #{filter[:operator]} ?", filter[:value]])
|
9
|
+
# Create an Abstract Active Record class and
|
10
|
+
# assign the table name from params
|
11
|
+
class_name = table_name.singularize.capitalize
|
12
|
+
if Object.const_defined? class_name
|
13
|
+
klass = class_name.constantize
|
14
|
+
else
|
15
|
+
klass = Object.const_set class_name, Class.new(DashApi::DashTable)
|
46
16
|
end
|
47
|
-
|
48
|
-
|
49
|
-
# Sorting
|
50
|
-
# Sort any field in ascending or descending direction
|
51
|
-
# Example: /books?order=title.asc
|
52
|
-
|
53
|
-
def sort(sort_by: nil, sort_direction: nil)
|
54
|
-
return self unless sort_by && sort_direction
|
55
|
-
@current_scope = @current_scope.order("#{sort_by}": sort_direction)
|
56
|
-
end
|
57
|
-
|
58
|
-
# Select
|
59
|
-
# Only return select fields from the table, equivalent to SQL select()
|
60
|
-
# Example: /books?select=id,title,summary
|
17
|
+
klass.table_name = table_name.downcase.pluralize
|
61
18
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
19
|
+
# Define a default Pundit policy class for this model
|
20
|
+
if Object.const_defined? "#{class_name}Policy"
|
21
|
+
policy = "#{class_name}Policy".constantize
|
22
|
+
else
|
23
|
+
policy = Object.const_set "#{class_name}Policy", Class.new(ApplicationPolicy)
|
24
|
+
end
|
66
25
|
|
67
|
-
|
68
|
-
|
69
|
-
# pg_search_scope is assigned dynamically against all table columns
|
70
|
-
# Example: books?keywords=Ruby+on+Rails
|
26
|
+
# Clear the cache since migrations don't restart the server.
|
27
|
+
klass.reset_column_information
|
71
28
|
|
72
|
-
|
73
|
-
|
74
|
-
self.pg_search_scope(
|
75
|
-
:pg_search,
|
76
|
-
against: self.searchable_fields,
|
77
|
-
using: {
|
78
|
-
tsearch: {
|
79
|
-
any_word: false
|
80
|
-
}
|
81
|
-
}
|
82
|
-
)
|
83
|
-
results = pg_search(keywords)
|
84
|
-
@current_scope = results
|
85
|
-
end
|
86
|
-
|
87
|
-
# Paginate
|
88
|
-
# Paginate the results with an offset and limit
|
89
|
-
# Example: books?page=1&per_page=20
|
29
|
+
# Guest the model associations using Rails naming conventions
|
30
|
+
klass.build_associations(includes) if includes.present?
|
90
31
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
offset = (page-1)*per_page
|
95
|
-
@current_scope = @current_scope.limit(per_page).offset(offset)
|
32
|
+
# Define the pg_search_scope for this model
|
33
|
+
klass.build_index_fields
|
34
|
+
klass
|
96
35
|
end
|
97
|
-
|
98
|
-
|
99
|
-
# We infer the association as either belongs_to or has_many
|
100
|
-
# based on whether the associated table name is singular or plural.
|
101
|
-
# We dynamically create the ActiveRecord class and set the appropriate
|
102
|
-
# ActiveRecord:Relation relationships
|
103
|
-
#
|
104
|
-
# Example: books?includes=author,reviews
|
105
|
-
|
106
|
-
def join_associations(associations: nil)
|
36
|
+
|
37
|
+
def build_associations(associations)
|
107
38
|
return nil unless associations
|
108
|
-
@associations = associations
|
109
39
|
associations.each do |table_name|
|
110
40
|
klass = DashTable.modelize(table_name)
|
111
41
|
if is_singular?(table_name)
|
112
42
|
self.belongs_to table_name.singularize.to_sym
|
113
43
|
klass.has_many self.table_name.pluralize.to_sym
|
114
|
-
@current_scope = @current_scope.includes(table_name.singularize.to_sym)
|
115
44
|
else
|
116
45
|
self.has_many table_name.pluralize.to_sym
|
117
46
|
klass.belongs_to self.table_name.singularize.to_sym
|
118
|
-
@current_scope = @current_scope.includes(table_name.pluralize.to_sym)
|
119
47
|
end
|
120
48
|
end
|
121
49
|
end
|
122
50
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
table_name.pluralize.to_sym
|
51
|
+
def build_index_fields
|
52
|
+
self.pg_search_scope(
|
53
|
+
:pg_search,
|
54
|
+
against: self.searchable_fields,
|
55
|
+
using: {
|
56
|
+
tsearch: {
|
57
|
+
any_word: false
|
58
|
+
}
|
132
59
|
}
|
133
|
-
|
134
|
-
include_hash = {}
|
135
|
-
associations_sym.each do |association|
|
136
|
-
include_hash.merge!({
|
137
|
-
"#{association}": {
|
138
|
-
except: DashApi.exclude_fields
|
139
|
-
}
|
140
|
-
})
|
141
|
-
end
|
142
|
-
|
143
|
-
@current_scope.as_json(
|
144
|
-
include: include_hash,
|
145
|
-
except: DashApi.exclude_fields
|
146
|
-
)
|
147
|
-
else
|
148
|
-
@current_scope.as_json(except: DashApi.exclude_fields)
|
149
|
-
end
|
60
|
+
)
|
150
61
|
end
|
151
62
|
|
152
63
|
def is_singular?(name)
|
153
64
|
name && name.singularize == name
|
154
65
|
end
|
155
66
|
|
156
|
-
def page_info
|
157
|
-
{
|
158
|
-
page: @page,
|
159
|
-
per_page: @per_page
|
160
|
-
}
|
161
|
-
end
|
162
|
-
|
163
|
-
def table_columns
|
164
|
-
return [] if self.table_name.nil?
|
165
|
-
self.columns
|
166
|
-
end
|
167
|
-
|
168
67
|
def searchable_fields
|
169
68
|
return [] if self.table_name.nil?
|
170
69
|
self.columns.map(&:name).filter{|column|
|
171
|
-
column unless DashApi.
|
70
|
+
column unless DashApi.exclude_attributes.include?(column.to_sym)
|
172
71
|
}
|
173
72
|
end
|
174
73
|
|
175
|
-
def modelize(table_name)
|
176
|
-
class_name = table_name.singularize.capitalize
|
177
|
-
if Object.const_defined? class_name
|
178
|
-
class_name.constantize
|
179
|
-
else
|
180
|
-
Object.const_set class_name, Class.new(DashApi::DashTable)
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
def foreign_keys
|
185
|
-
self.columns.map{|col|
|
186
|
-
if col.name.downcase.split("_")[-1] == 'id' && col.name.downcase != 'id'
|
187
|
-
col.name
|
188
|
-
end
|
189
|
-
}.compact
|
190
|
-
end
|
191
|
-
|
192
|
-
def foreign_table(foreign_key)
|
193
|
-
foreign_key.downcase.split('_').first.pluralize
|
194
|
-
end
|
195
|
-
|
196
|
-
def foreign_tables
|
197
|
-
foreign_keys.map{ |foreign_key|
|
198
|
-
foreign_table(foreign_key)
|
199
|
-
}
|
200
|
-
end
|
201
74
|
end
|
202
75
|
end
|
203
76
|
end
|
@@ -1,19 +1,39 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
|
1
4
|
module DashApi
|
2
5
|
module JsonWebToken
|
3
|
-
require 'jwt'
|
4
|
-
|
6
|
+
require 'jwt'
|
7
|
+
|
5
8
|
JWT_HASH_ALGORITHM = 'HS256'
|
9
|
+
GOOGLE_CERTS_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
|
6
10
|
|
7
11
|
def self.encode(payload:, expiration:)
|
8
|
-
payload[:exp] = expiration ||
|
9
|
-
JWT.encode(payload, DashApi.jwt_secret, DashApi.jwt_hash_algorithm ||JWT_HASH_ALGORITHM)
|
12
|
+
payload[:exp] = expiration || 15.minutes.from_now.to_i
|
13
|
+
JWT.encode(payload, DashApi.jwt_secret, DashApi.jwt_hash_algorithm || JWT_HASH_ALGORITHM)
|
10
14
|
end
|
11
15
|
|
12
16
|
def self.decode(jwt_token)
|
13
|
-
|
17
|
+
jwt = JWT.decode(jwt_token, DashApi.jwt_secret, true, {
|
14
18
|
algorithm: DashApi.jwt_algorithm || JWT_HASH_ALGORITHM
|
15
|
-
})
|
19
|
+
})
|
20
|
+
HashWithIndifferentAccess.new(jwt[0])
|
16
21
|
end
|
17
22
|
|
23
|
+
def self.decode_firebase(jwt_token)
|
24
|
+
firebase_web_key = DashApi.firebase_web_key
|
25
|
+
jwt = JWT.decode(jwt_token, firebase_web_key, true, { algorithm: 'RS256' }) do |header|
|
26
|
+
url = URI(GOOGLE_CERTS_URL)
|
27
|
+
json = JSON.parse(Net::HTTP.get(url))
|
28
|
+
public_key = OpenSSL::X509::Certificate.new(json[header['kid']]).public_key
|
29
|
+
end
|
30
|
+
jwt = jwt[0].merge({ role: "user", provider: "firebase" })
|
31
|
+
HashWithIndifferentAccess.new(jwt)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.decode_unverified(jwt_token)
|
35
|
+
HashWithIndifferentAccess.new(JWT.decode(jwt_token, nil, false)[0])
|
36
|
+
end
|
37
|
+
|
18
38
|
end
|
19
39
|
end
|
@@ -30,35 +30,30 @@
|
|
30
30
|
select_fields = params[:select]&.split(',')
|
31
31
|
end
|
32
32
|
|
33
|
-
if params[:order]
|
33
|
+
if params[:order]
|
34
34
|
sort_by, sort_direction = params[:order].split(DELIMITER)
|
35
35
|
sort_direction = "desc" if sort_direction and !SORT_DIRECTIONS.include?(sort_direction)
|
36
|
+
order = { "#{sort_by}": sort_direction }
|
36
37
|
end
|
37
38
|
|
38
39
|
if params[:includes]
|
39
40
|
associations = params[:includes].split(",").map(&:strip)
|
40
41
|
end
|
41
42
|
|
42
|
-
filters = []
|
43
|
+
filters = []
|
43
44
|
if params[:filters]
|
44
|
-
params[:filters].split(',').each do |filter_param|
|
45
|
-
|
46
|
-
|
47
|
-
operator = OPERATORS[rel.to_sym] || '='
|
48
|
-
filters << {
|
49
|
-
field: field,
|
50
|
-
operator: operator,
|
51
|
-
value: value
|
52
|
-
}
|
53
|
-
end
|
45
|
+
params[:filters].split(',').each do |filter_param|
|
46
|
+
filters << format_filter(filter_param)
|
47
|
+
end
|
54
48
|
end
|
55
|
-
|
49
|
+
|
56
50
|
page = params[:page]&.to_i || 1
|
57
51
|
per_page = params[:per_page]&.to_i || PER_PAGE
|
58
52
|
|
59
53
|
{
|
60
54
|
keywords: keywords,
|
61
55
|
select_fields: select_fields,
|
56
|
+
order: order,
|
62
57
|
sort_by: sort_by,
|
63
58
|
sort_direction: sort_direction,
|
64
59
|
filters: filters,
|
@@ -68,5 +63,13 @@
|
|
68
63
|
}
|
69
64
|
end
|
70
65
|
|
66
|
+
def self.format_filter(filter_param)
|
67
|
+
field, rel, value = filter_param.split(DELIMITER)
|
68
|
+
rel = "eq" unless OPERATORS.keys.include?(rel.to_sym)
|
69
|
+
operator = OPERATORS[rel.to_sym] || '='
|
70
|
+
condition = "#{field} #{operator} ?"
|
71
|
+
[condition, value]
|
72
|
+
end
|
73
|
+
|
71
74
|
end
|
72
75
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DashApi
|
2
|
+
module Serializer
|
3
|
+
|
4
|
+
def self.to_json(current_scope, includes: nil)
|
5
|
+
opts = { except: DashApi.exclude_attributes }
|
6
|
+
if includes
|
7
|
+
include_hash = {}
|
8
|
+
includes.each {|table_name| include_hash.merge!({
|
9
|
+
"#{table_name}": { except: DashApi.exclude_attributes }
|
10
|
+
})
|
11
|
+
}
|
12
|
+
current_scope.as_json(opts.merge(include: include_hash))
|
13
|
+
else
|
14
|
+
current_scope.as_json(opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/dash_api/version.rb
CHANGED
data/lib/dash_api.rb
CHANGED
@@ -3,13 +3,12 @@ require "dash_api/engine"
|
|
3
3
|
|
4
4
|
module DashApi
|
5
5
|
|
6
|
-
mattr_accessor :enable_auth
|
7
6
|
mattr_accessor :jwt_secret
|
8
7
|
mattr_accessor :jwt_algorithm
|
9
8
|
|
10
|
-
mattr_accessor :
|
9
|
+
mattr_accessor :firebase_web_key
|
11
10
|
|
12
|
-
mattr_accessor :
|
11
|
+
mattr_accessor :exclude_attributes
|
13
12
|
mattr_accessor :exclude_tables
|
14
13
|
|
15
14
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dash_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.22
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rami Bitar
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -31,7 +31,7 @@ dependencies:
|
|
31
31
|
- !ruby/object:Gem::Version
|
32
32
|
version: 6.1.4.1
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: dotenv-rails
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - ">="
|
@@ -45,7 +45,7 @@ dependencies:
|
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: jwt
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
requirements:
|
51
51
|
- - ">="
|
@@ -73,7 +73,7 @@ dependencies:
|
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0'
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
|
-
name:
|
76
|
+
name: ostruct
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - ">="
|
@@ -87,7 +87,35 @@ dependencies:
|
|
87
87
|
- !ruby/object:Gem::Version
|
88
88
|
version: '0'
|
89
89
|
- !ruby/object:Gem::Dependency
|
90
|
-
name:
|
90
|
+
name: pundit
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: pg
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :runtime
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: pg_search
|
91
119
|
requirement: !ruby/object:Gem::Requirement
|
92
120
|
requirements:
|
93
121
|
- - ">="
|
@@ -113,6 +141,8 @@ files:
|
|
113
141
|
- Rakefile
|
114
142
|
- app/assets/config/dash_manifest.js
|
115
143
|
- app/assets/stylesheets/dash/application.css
|
144
|
+
- app/controllers/concerns/dash_api/api_exception.rb
|
145
|
+
- app/controllers/concerns/dash_api/auth.rb
|
116
146
|
- app/controllers/dash_api/api_controller.rb
|
117
147
|
- app/controllers/dash_api/application_controller.rb
|
118
148
|
- app/controllers/dash_api/schema_controller.rb
|
@@ -125,6 +155,7 @@ files:
|
|
125
155
|
- app/services/dash_api/json_web_token.rb
|
126
156
|
- app/services/dash_api/query.rb
|
127
157
|
- app/services/dash_api/schema.rb
|
158
|
+
- app/services/dash_api/serializer.rb
|
128
159
|
- app/views/layouts/dash_api/application.html.erb
|
129
160
|
- config/routes.rb
|
130
161
|
- lib/dash_api.rb
|
@@ -153,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
184
|
- !ruby/object:Gem::Version
|
154
185
|
version: '0'
|
155
186
|
requirements: []
|
156
|
-
rubygems_version: 3.
|
187
|
+
rubygems_version: 3.2.22
|
157
188
|
signing_key:
|
158
189
|
specification_version: 4
|
159
190
|
summary: REST API for your postgres database.
|