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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 945983e9aaba31d04f8fde011da5a657e2d0d6159e6586e83ba8f6c03fa6653f
4
- data.tar.gz: 24bc114dc3585acf020fcca25d0795a7da97edab9b433d37913fb9cbd99b7afb
3
+ metadata.gz: ab623028d1c6a931fef116ca98b2b990782464c78648005accd420039dcfd2be
4
+ data.tar.gz: bcbb8bb65b2805bfe936a232ee9cb5ed1863b9d7a44582d26c719336693a4a88
5
5
  SHA512:
6
- metadata.gz: 68eba8c684f9193eeb6495852659941655d66ef24a9ac7ae33201fa9e8e93ad384476eef60e9060ca8771b846a634f4b24b40bc36cd2b5bffcf8dcda0eae467e
7
- data.tar.gz: d29484f82a3715c8af5fe7703e0115818c04ecd2a9be2ac5dd4eeaab672c0d7a4879990560cfb280e9c674de8925cff31fc125e8e9f94a965f109107be6474e4
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
- Or install it yourself as:
44
+ Install the pundit policy
43
45
  ```bash
44
- $ gem install dash_api
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
- ### API Token Authentication
302
+ ### JWT Token Authorization
300
303
 
301
- You may secure the Dash API using a dedicated API token or using a JWT token.
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
- For a fast and simple way to secure your API, you can specify an api_token in `config/initializers/dash_api.rb` which will allow all API requests.
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.enable_auth = true
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
- Ensure that the enable authentication flag `enable_auth` is set to `true`.
317
-
318
-
319
- ### JWT Token Authentication
321
+ ### Firebase Authorization
320
322
 
321
- The recommended and preferred way to secure your API is to use a JWT token. To enable a JWT token, you must first
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.enable_auth = true
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
- Ensure that the enable authentication flag `enable_auth` is set to `true`.
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
- ### API Authorization
358
+ ### Authorization (Pundit)
347
359
 
348
- Once you've setup your authentication strategy above, either using the `api_token` or the `jwt_secret`,
349
- you should pass your token to the DashAPI either as a URL parameter or using Bearer authentication
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
- ?token=<JWT_TOKEN>
369
+ # Place this file in /app/policies/order_policy.rb
370
+
371
+ def OrderPolicy < ApplicationPolicy
372
+ ...
373
+ end
354
374
  ```
355
375
 
356
- The preferred strategy is to pass the token using Bearer authentication in your headers:
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
- Authorization: 'Bearer <JWT_TOKEN>'
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
- ### Exclude fields and tables
440
+ ### Authorization for CRUD operations
363
441
 
364
- The Dash API is not yet suitable for production scale applications. Please use with caution.
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
- You can exclude fields from being serialized by specifying DASH_EXCLUDE_FIELDS
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.exclude_fields = ENV['DASH_EXCLUDE_FIELDS'].split(' ') || []
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.exclude_fields = "encrypted_password hashed_password secret_token"
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
- @dash_table.query(
12
- keywords: @query[:keywords],
13
- select_fields: @query[:select_fields],
14
- sort_by: @query[:sort_by],
15
- sort_direction: @query[:sort_direction],
16
- filters: @query[:filters],
17
- associations: @query[:associations],
18
- page: @query[:page],
19
- per_page: @query[:per_page]
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: @dash_table.serialize,
24
- meta: @dash_table.page_info
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 = @dash_table.query(
30
- associations: @query[:associations],
31
- filters: [{ field: :id, operator: "=", value: params[:id] }],
32
- page: 1,
33
- per_page: 1
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 = @dash_table.create!(dash_params)
40
- render json: { data: resource }
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 = @dash_table.find(params[:id])
47
+ resource = dash_scope.find(params[:id])
48
+ authorize resource, :update?
45
49
  if resource.update(dash_params)
46
- render json: { data: resource }
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 = @dash_table.find(params[:id])
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 = @dash_table.where(id: params[:ids])
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 = @dash_table.where(id: params[:ids])
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
- @query = DashApi::Query.parse(params)
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 load_table
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
- @dash_table.table_name = params[:table_name]
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
- rescue_from Exception, with: :unprocessable_entity
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
@@ -1,7 +1,5 @@
1
1
  module DashApi
2
2
  class SchemaController < ApplicationController
3
-
4
- before_action :authenticate_request!
5
3
 
6
4
  def index
7
5
  tables = DashApi::Schema.table_names
@@ -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
- attr_accessor :scope, :includes
7
+ def modelize(table_name, includes: nil)
8
8
 
9
- def query(
10
- keywords: nil,
11
- select_fields: nil,
12
- associations: nil,
13
- sort_by: 'id',
14
- sort_direction: 'asc',
15
- filters: nil,
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
- end
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
- def select_fields(fields: nil)
63
- return unless fields
64
- @current_scope = @current_scope.select(fields)
65
- end
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
- # Search
68
- # Full-text search using the pg_search gem
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
- def full_text_search(keywords: nil)
73
- return if keywords.blank?
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
- def paginate(per_page: 20, page: 1)
92
- @page = page
93
- @per_page = per_page
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
- # Join associations
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
- # Serialize
124
- # We serialize with the appropriate associations
125
- # based on whether the assocation is singular or plural.
126
- def serialize
127
- if @associations
128
- associations_sym = @associations.map{|table_name|
129
- is_singular?(table_name) ?
130
- table_name.singularize.to_sym :
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.exclude_fields.include?(column.to_sym)
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
@@ -5,5 +5,5 @@ module DashApi
5
5
 
6
6
  self.abstract_class = true
7
7
 
8
- end
8
+ end
9
9
  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 || Time.now.advance(minutes: 15)
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
- HashWithIndifferentAccess.new(JWT.decode(jwt_token, DashApi.jwt_secret, true, {
17
+ jwt = JWT.decode(jwt_token, DashApi.jwt_secret, true, {
14
18
  algorithm: DashApi.jwt_algorithm || JWT_HASH_ALGORITHM
15
- })[0])
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
- field, rel, value = filter_param.split(DELIMITER)
46
- rel = "eq" unless OPERATORS.keys.include?(rel.to_sym)
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
@@ -1,3 +1,3 @@
1
1
  module DashApi
2
- VERSION = '0.0.16'
2
+ VERSION = '0.0.22'
3
3
  end
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 :api_token
9
+ mattr_accessor :firebase_web_key
11
10
 
12
- mattr_accessor :exclude_fields
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.16
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-10-31 00:00:00.000000000 Z
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: pg
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: pg_search
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: dotenv-rails
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: jwt
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.1.6
187
+ rubygems_version: 3.2.22
157
188
  signing_key:
158
189
  specification_version: 4
159
190
  summary: REST API for your postgres database.