schema_based_api 2.1.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d0fa11327240df5877da54b0b1809baa1ed67740a2cc1223738d89965e28a91
4
+ data.tar.gz: ef0065c4a0853d41ff56552189f6fb3290209b1b52dd4c4b6f7d85199673495e
5
+ SHA512:
6
+ metadata.gz: 3fac7557321ca4e7b3a49e81318cfb60e85ec2f69836575b11f49ed1833a938749a8662913c7c0014ed59cd635847f226a2285aeac2c3c5712aef4b0c0589b28
7
+ data.tar.gz: 9ccd8783f5e30a4b308ec2397da59a9fd66c205e2bf547d72b1bc45077028b900faf59d5eba8cf73091e2df1ea4e8498384cca8b2176f22399a4d5e0c733f372
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2020
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # SchemaBasedApi
2
+ I've always been a interested in effortless, no-fuss, conventions' based development, DRYness, and pragmatic programming, I've always thought that at this point of the technology evolution, we need not to configure too much to have our software run, and have the software adapt to data layers, from there build up automatically the APIs, visualizations etc. This is a first step to have a schema driven API, based on the data it has to serve, it also gives, thanks to meta programming, an insight on the actual schema, the translations available and the DSL which can change the way the data is presented, leading to a strong base for automatically build of UIs consuming the API (react, vue, angular based PWAs, maybe! ;-) ).
3
+
4
+ Doing this means also narrowing a bit the scope of the tools, taking decisions, at least for the first iteratons of the project, so, this works well if the data is relational, so this is a convention taken as a prerequisite (postgres, mysql, mssql, etc.).
5
+
6
+ # Goal
7
+
8
+ To have a comprehensive and meaningful API right out of the box by just creating migrations in your rails app.
9
+
10
+ # v2?
11
+
12
+ Yes, the [v1](https://github.com/gabrieletassoni/thecore_api) was were it all started, many ideas are ported from there, but it was too coupled with thecore's rails_admin UI, making it impossible to create an UI-less, API only application, out of the box and directly from the DB schema, with all the bells and whistles I needed (mainly self adapting, data and schema driven API functionalities).
13
+
14
+ # Standards Used
15
+
16
+ * [JWT](https://medium.com/@billy.sf.cheng/a-rails-6-application-part-1-api-1ee5ccf7ed01) for authentication.
17
+ * [CanCanCan](https://github.com/CanCanCommunity/cancancan) for authorization.
18
+ * [Active Hash Relation](https://github.com/kollegorna/active_hash_relation) for DSL.
19
+ * [Ransack](https://github.com/activerecord-hackery/ransack) query engine for complex searches going beyond CRUD's listing scope.
20
+ * Catch all routing rule to add basic crud operations to any AR model in the app.
21
+
22
+ # TODO
23
+
24
+ * Integrate Authorization within ```GET info/schema``` requests in order to send to the client just the models for which a user has authorization.
25
+
26
+ ## Usage
27
+ How to use my plugin.
28
+
29
+ ## Installation
30
+ Add this line to your application's Gemfile:
31
+
32
+ ```ruby
33
+ gem 'schema_based_api'
34
+ ```
35
+
36
+ And then execute:
37
+ ```bash
38
+ $ bundle
39
+ ```
40
+
41
+ Or install it yourself as:
42
+ ```bash
43
+ $ gem install schema_based_api
44
+ ```
45
+
46
+ Then run the migrations:
47
+ ```bash
48
+ $ rails db:migrate
49
+ ```
50
+
51
+ This will setup a User model, Role model and the HABTM table between the two.
52
+
53
+ Then, if you fire up your ```rails server``` you can already get a jwt and perform different operations.
54
+ The default admin user created during the migration step has a randomly generated password you can find in a .passwords file in the root of your project, that's the initial password, in production you can replace that one, but for testing it proved handy to have it promptly available.
55
+
56
+ If you want to manually test the API using [Insomnia](https://insomnia.rest/)
57
+
58
+ ## References
59
+ THanks to all these people for ideas:
60
+
61
+ * [Daniel](https://medium.com/@tdaniel/passing-refreshed-jwts-from-rails-api-using-headers-859f1cfe88e9) For a smart way to manage token expiration.
62
+
63
+ ## License
64
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'SchemaBasedApi'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+ task default: :test
@@ -0,0 +1,32 @@
1
+ class AuthenticateUser
2
+ class AccessDenied < StandardError
3
+ def message
4
+ "Cannot authenticate user."
5
+ end
6
+ end
7
+ prepend SimpleCommand
8
+
9
+ def initialize(email, password)
10
+ @email = email
11
+ @password = password
12
+ end
13
+
14
+ def call
15
+ JsonWebToken.encode(user_id: api_user.id) if api_user
16
+ end
17
+
18
+ private
19
+
20
+ attr_accessor :email, :password
21
+
22
+ def api_user
23
+ user = User.find_by_email(email)
24
+ raise AccessDenied unless user.present?
25
+
26
+ # Verify the password. You can create a blank method for now.
27
+ raise AccessDenied if user.authenticate(password).blank?
28
+
29
+ return user
30
+ end
31
+
32
+ end
@@ -0,0 +1,33 @@
1
+ class AuthorizeApiRequest
2
+ prepend SimpleCommand
3
+
4
+ def initialize(headers = {})
5
+ @headers = headers
6
+ end
7
+
8
+ def call
9
+ api_user
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :headers
15
+
16
+ def api_user
17
+ @api_user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
18
+ @api_user || errors.add(:token, "Invalid token") && nil
19
+ end
20
+
21
+ def decoded_auth_token
22
+ @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
23
+ end
24
+
25
+ def http_auth_header
26
+ if headers['Authorization'].present?
27
+ return headers['Authorization'].split(' ').last
28
+ else
29
+ errors.add(:token, "Missing token")
30
+ end
31
+ nil
32
+ end
33
+ end
@@ -0,0 +1,184 @@
1
+ class Api::V2::ApplicationController < ActionController::API
2
+ include ActiveHashRelation
3
+
4
+ before_action :authenticate_request
5
+ before_action :extract_model
6
+ before_action :find_record, only: [ :show, :destroy, :update ]
7
+
8
+ attr_accessor :current_user
9
+
10
+ # Actions will be authorized directly in the action
11
+ include CanCan::ControllerAdditions
12
+
13
+ include ::ApiExceptionManagement
14
+
15
+ # Nullifying strong params for API
16
+ def params
17
+ request.parameters
18
+ end
19
+
20
+ # TODO: Remove when not needed
21
+ # def dispatcher
22
+ # # This method is only valid for ActiveRecords
23
+ # # For any other model-less controller, the actions must be
24
+ # # defined in the route, and must exist in the controller definition.
25
+ # # So, if it's not an activerecord, the find model makes no sense at all.
26
+ # path = params[:path].split("/")
27
+ # # Default convention for the requests: :controller/:id/:custom_action
28
+ # # or :controller/:custom_action.
29
+ # # With the ID as an Integer
30
+ # # TODO: Extend to understand nested resources maybe testing if the
31
+ # # third param is a AR model, that can have an ID, etc..
32
+ # controller = path.first
33
+ # id = path.second
34
+ # custom_action = path.third
35
+ # # managing
36
+ # if request.get?
37
+ # if id.blank?
38
+ # # @page = params[:page]
39
+ # # @per = params[:per]
40
+ # # @pages_info = params[:pages_info]
41
+ # # @count = params[:count]
42
+ # # @query = params[:q]
43
+ # index
44
+ # elsif id.to_i.zero?
45
+ # # String, so it's a custom action I must find in the @model (as a singleton method)
46
+ # # GET :controller/:custom_action
47
+ # return not_found! unless @model.respond_to?(id)
48
+ # return render json: MultiJson.dump(@model.send(id, params)), status: 200
49
+ # elsif !id.to_i.zero? && custom_action.blank?
50
+ # # Integer, so it's an ID, I must show it
51
+ # @record_id = id.to_i
52
+ # find_record
53
+ # show
54
+ # elsif !id.to_i.zero? && !custom_action.blank?
55
+ # # GET :controller/:id/:custom_action
56
+ # return not_found! unless @model.respond_to?(custom_action)
57
+ # return render json: MultiJson.dump(@model.send(custom_action, id.to_i, params)), status: 200
58
+ # end
59
+ # elsif request.post?
60
+ # if id.blank?
61
+ # # @params = params
62
+ # create
63
+ # elsif id.to_i.zero?
64
+ # # POST :controller/:custom_action
65
+ # return not_found! unless @model.respond_to?(id)
66
+ # return render json: MultiJson.dump(@model.send(id, params)), status: 200
67
+ # end
68
+ # elsif request.put?
69
+ # if !id.to_i.zero? && custom_action.blank?
70
+ # # @params = params
71
+ # # Rails.logger.debug "IL SECONDO è ID in PUT? #{path.second.inspect}"
72
+ # # find_record path.second.to_i
73
+ # @record_id = id.to_i
74
+ # find_record
75
+ # update
76
+ # elsif !id.to_i.zero? && !custom_action.blank?
77
+ # # PUT :controller/:id/:custom_action
78
+ # # puts "ANOTHER SECOND AND THIRD"
79
+ # return not_found! unless @model.respond_to?(custom_action)
80
+ # return render json: MultiJson.dump(@model.send(custom_action, id.to_i, params)), status: 200
81
+ # end
82
+ # elsif request.delete?
83
+ # # Rails.logger.debug "IL SECONDO è ID in delete? #{path.second.inspect}"
84
+ # # find_record path.second.to_i
85
+ # @record_id = id.to_i
86
+ # find_record
87
+ # destroy
88
+ # end
89
+ # end
90
+
91
+ # GET :controller/
92
+ def index
93
+ authorize! :index, @model
94
+ # Rails.logger.debug params.inspect
95
+ # find the records
96
+ @q = (@model.column_names.include?("user_id") ? @model.where(user_id: current_user.id) : @model).ransack(@query.presence|| params[:q])
97
+ @records_all = @q.result(distinct: true)
98
+ page = (@page.presence || params[:page])
99
+ per = (@per.presence || params[:per])
100
+ pages_info = (@pages_info.presence || params[:pages_info])
101
+ count = (@count.presence || params[:count])
102
+ # Paging
103
+ @records = @records_all.page(page).per(per)
104
+
105
+ # If there's the keyword pagination_info, then return a pagination info object
106
+ return render json: MultiJson.dump({count: @records_all.count,current_page_count: @records.count,next_page: @records.next_page,prev_page: @records.prev_page,is_first_page: @records.first_page?,is_last_page: @records.last_page?,is_out_of_range: @records.out_of_range?,pages_count: @records.total_pages,current_page_number: @records.current_page }) if !pages_info.blank?
107
+
108
+ # puts "ALL RECORDS FOUND: #{@records_all.inspect}"
109
+ status = @records_all.blank? ? 404 : 200
110
+ # puts "If it's asked for page number, then paginate"
111
+ return render json: MultiJson.dump(@records, json_attrs), status: status if !page.blank? # (@json_attrs || {})
112
+ #puts "if you ask for count, then return a json object with just the number of objects"
113
+ return render json: MultiJson.dump({count: @records_all.count}) if !count.blank?
114
+ #puts "Default"
115
+ json_out = MultiJson.dump(@records_all, json_attrs)
116
+ #puts "JSON ATTRS: #{json_attrs}"
117
+ #puts "JSON OUT: #{json_out}"
118
+ render json: json_out, status: status #(@json_attrs || {})
119
+ end
120
+
121
+ def show
122
+ authorize! :show, @record
123
+ result = @record.to_json(json_attrs)
124
+ render json: result, status: 200
125
+ end
126
+
127
+ def create
128
+ @record = @model.new(@body)
129
+ authorize! :create, @record
130
+ @record.user_id = current_user.id if @model.column_names.include? "user_id"
131
+
132
+ @record.save!
133
+
134
+ render json: @record.to_json(json_attrs), status: 201
135
+ end
136
+
137
+ def update
138
+ authorize! :update, @record
139
+ @record.update_attributes!(@body)
140
+
141
+ render json: @record.to_json(json_attrs), status: 200
142
+ end
143
+
144
+ def destroy
145
+ authorize! :destroy, @record
146
+ return api_error(status: 500) unless @record.destroy
147
+ head :ok
148
+ end
149
+
150
+ private
151
+
152
+ def authenticate_request
153
+ @current_user = AuthorizeApiRequest.call(request.headers).result
154
+ return unauthenticated! unless @current_user
155
+ current_user = @current_user
156
+ # Now every time the user fires off a successful GET request,
157
+ # a new token is generated and passed to them, and the clock resets.
158
+ response.headers['Token'] = JsonWebToken.encode(user_id: current_user.id)
159
+ end
160
+
161
+ def find_record
162
+ record_id ||= (params[:path].split("/").second.to_i rescue nil)
163
+ @record = @model.column_names.include?("user_id") ? @model.where(id: (record_id.presence || @record_id.presence || params[:id]), user_id: current_user.id).first : @model.find((@record_id.presence || params[:id]))
164
+ return not_found! if @record.blank?
165
+ end
166
+
167
+ def json_attrs
168
+ ((@model.json_attrs.presence || @json_attrs.presence || {}) rescue {})
169
+ end
170
+
171
+ def extract_model
172
+ # This method is only valid for ActiveRecords
173
+ # For any other model-less controller, the actions must be
174
+ # defined in the route, and must exist in the controller definition.
175
+ # So, if it's not an activerecord, the find model makes no sense at all
176
+ # thus must return 404.
177
+ @model = (params[:ctrl].classify.constantize rescue params[:path].split("/").first.classify.constantize rescue controller_path.classify.constantize rescue controller_name.classify.constantize rescue nil)
178
+ # Getting the body of the request if it exists, it's ok the singular or
179
+ # plural form, this helps with automatic tests with Insomnia.
180
+ @body = params[@model.model_name.singular].presence || params[@model.model_name.route_key]
181
+ # Only ActiveRecords can have this model caputed
182
+ return not_found! if (!@model.new.is_a? ActiveRecord::Base rescue false)
183
+ end
184
+ end
@@ -0,0 +1,15 @@
1
+ class Api::V2::AuthenticationController < ActionController::API
2
+ include ::ApiExceptionManagement
3
+
4
+ def authenticate
5
+ command = AuthenticateUser.call(params[:auth][:email], params[:auth][:password])
6
+
7
+ if command.success?
8
+ response.headers['Token'] = command.result
9
+ render json: { message: ["Login successful!"] }
10
+ else
11
+ render json: { error: command.errors }, status: :unauthorized
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,68 @@
1
+ class Api::V2::InfoController < Api::V2::ApplicationController
2
+ # Info uses a different auth method: username and password
3
+ skip_before_action :authenticate_request, only: [:version], raise: false
4
+ skip_before_action :extract_model
5
+
6
+ # api :GET, '/api/v2/info/version', "Just prints the APPVERSION."
7
+ def version
8
+ render json: { version: SchemaBasedApi::VERSION }.to_json, status: 200
9
+ end
10
+
11
+ # api :GET, '/api/v2/info/roles'
12
+ # it returns the roles list
13
+ def roles
14
+ render json: ::Role.all.to_json, status: 200
15
+ end
16
+
17
+ # GET '/api/v2/info/translations'
18
+ def translations
19
+ render json: I18n.t(".", locale: (params[:locale].presence || :it)).to_json, status: 200
20
+ end
21
+
22
+ # GET '/api/v2/info/schema'
23
+ def schema
24
+ pivot = {}
25
+ # if Rails.env.development?
26
+ # Rails.configuration.eager_load_namespaces.each(&:eager_load!) if Rails.version.to_i == 5 #Rails 5
27
+ # Zeitwerk::Loader.eager_load_all if Rails.version.to_i >= 6 #Rails 6
28
+ # end
29
+ ApplicationRecord.subclasses.each do |d|
30
+ # Only if current user can read the model
31
+ if can? :read, d
32
+ model = d.to_s.underscore.tableize
33
+ pivot[model] ||= {}
34
+ d.columns_hash.each_pair do |key, val|
35
+ pivot[model][key] = val.type unless key.ends_with? "_id"
36
+ end
37
+ # Only application record descendants to have a clean schema
38
+ pivot[model][:associations] ||= {
39
+ has_many: d.reflect_on_all_associations(:has_many).map { |a|
40
+ a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
41
+ }.compact,
42
+ belongs_to: d.reflect_on_all_associations(:belongs_to).map { |a|
43
+ a.name if (((a.options[:class_name].presence || a.name).to_s.classify.constantize.new.is_a? ApplicationRecord) rescue false)
44
+ }.compact
45
+ }
46
+ pivot[model][:methods] ||= (d.instance_methods(false).include?(:json_attrs) && !d.json_attrs.blank?) ? d.json_attrs[:methods] : nil
47
+ end
48
+ end
49
+ render json: pivot.to_json, status: 200
50
+ end
51
+
52
+ # GET '/api/v2/info/dsl'
53
+ def dsl
54
+ pivot = {}
55
+ # if Rails.env.development?
56
+ # Rails.configuration.eager_load_namespaces.each(&:eager_load!) if Rails.version.to_i == 5 #Rails 5
57
+ # Zeitwerk::Loader.eager_load_all if Rails.version.to_i >= 6 #Rails 6
58
+ # end
59
+ ApplicationRecord.subclasses.each do |d|
60
+ # Only if current user can read the model
61
+ if can? :read, d
62
+ model = d.to_s.underscore.tableize
63
+ pivot[model] = (d.instance_methods(false).include?(:json_attrs) && !d.json_attrs.blank?) ? d.json_attrs : nil
64
+ end
65
+ end
66
+ render json: pivot.to_json, status: 200
67
+ end
68
+ end
@@ -0,0 +1,9 @@
1
+ class Api::V2::UsersController < Api::V2::ApplicationController
2
+ before_action :check_demoting, only: [ :update, :destroy ]
3
+
4
+ private
5
+
6
+ def check_demoting
7
+ unauthorized! StandardError.new("You cannot demote yourself") if (params[:id].to_i == current_user.id && (params[:user].keys.include?("admin") || params[:user].keys.include?("locked")))
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require 'concerns/schema_based_api_user'
2
+
3
+ Rails.application.configure do
4
+ config.after_initialize do
5
+ User.send(:include, SchemaBasedApiUser)
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ # config/initializers/cors.rb
2
+ Rails.application.config.middleware.insert_before 0, Rack::Cors do
3
+ allow do
4
+ origins '*'
5
+ resource '*',
6
+ headers: %w(Token),
7
+ methods: :any,
8
+ expose: %w(Token),
9
+ max_age: 600
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # Knock.setup do |config|
2
+
3
+ # ## Expiration claim
4
+ # ## ----------------
5
+ # ##
6
+ # ## How long before a token is expired. If nil is provided, token will
7
+ # ## last forever.
8
+ # ##
9
+ # ## Default:
10
+ # # config.token_lifetime = 1.day
11
+
12
+
13
+ # ## Audience claim
14
+ # ## --------------
15
+ # ##
16
+ # ## Configure the audience claim to identify the recipients that the token
17
+ # ## is intended for.
18
+ # ##
19
+ # ## Default:
20
+ # # config.token_audience = nil
21
+
22
+ # ## If using Auth0, uncomment the line below
23
+ # # config.token_audience = -> { Rails.application.secrets.auth0_client_id }
24
+
25
+ # ## Signature algorithm
26
+ # ## -------------------
27
+ # ##
28
+ # ## Configure the algorithm used to encode the token
29
+ # ##
30
+ # ## Default:
31
+ # # config.token_signature_algorithm = 'HS256'
32
+
33
+ # ## Signature key
34
+ # ## -------------
35
+ # ##
36
+ # ## Configure the key used to sign tokens.
37
+ # ##
38
+ # ## Default:
39
+ # # config.token_secret_signature_key = -> { Rails.application.secrets.secret_key_base }
40
+
41
+ # ## If using Auth0, uncomment the line below
42
+ # # config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret }
43
+
44
+ # ## Public key
45
+ # ## ----------
46
+ # ##
47
+ # ## Configure the public key used to decode tokens, if required.
48
+ # ##
49
+ # ## Default:
50
+ # # config.token_public_key = nil
51
+
52
+ # ## Exception Class
53
+ # ## ---------------
54
+ # ##
55
+ # ## Configure the exception to be used when user cannot be found.
56
+ # ##
57
+ # ## Default:
58
+ # # config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
59
+ # end
@@ -0,0 +1,14 @@
1
+ # Be sure to restart your server when you modify this file.
2
+
3
+ # This file contains settings for ActionController::ParamsWrapper which
4
+ # is enabled by default.
5
+
6
+ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7
+ ActiveSupport.on_load(:action_controller) do
8
+ wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9
+ end
10
+
11
+ # To enable root element in JSON for ActiveRecord objects.
12
+ # ActiveSupport.on_load(:active_record) do
13
+ # self.include_root_in_json = true
14
+ # end
data/config/routes.rb ADDED
@@ -0,0 +1,33 @@
1
+ # require 'ransack'
2
+
3
+ Rails.application.routes.draw do
4
+ # REST API (Stateless)
5
+ namespace :api, constraints: { format: :json } do
6
+ namespace :v2 do
7
+ resources :users
8
+
9
+ namespace :info do
10
+ get :version
11
+ get :roles
12
+ get :translations
13
+ get :schema
14
+ get :dsl
15
+ end
16
+
17
+ post "authenticate" => "authentication#authenticate"
18
+ post ":ctrl/search" => 'application#index'
19
+
20
+ # Catchall routes
21
+ # # CRUD Show
22
+ get '*path/:id', to: 'application#show'
23
+ # # CRUD Index
24
+ get '*path', to: 'application#index'
25
+ # # CRUD Create
26
+ post '*path', to: 'application#create'
27
+ # # CRUD Update
28
+ put '*path/:id', to: 'application#update'
29
+ # # CRUD DElete
30
+ delete '*path/:id', to: 'application#destroy'
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,67 @@
1
+ module ApiExceptionManagement
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ rescue_from NoMethodError, with: :not_found!
6
+ rescue_from CanCan::AccessDenied, with: :unauthorized!
7
+ rescue_from AuthenticateUser::AccessDenied, with: :unauthenticated!
8
+ rescue_from ActionController::RoutingError, with: :not_found!
9
+ rescue_from ActiveModel::ForbiddenAttributesError, with: :fivehundred!
10
+ rescue_from ActiveRecord::RecordInvalid, with: :invalid!
11
+ rescue_from ActiveRecord::RecordNotFound, with: :not_found!
12
+
13
+ def unauthenticated! exception = StandardError.new
14
+ response.headers['WWW-Authenticate'] = "Token realm=Application"
15
+ api_error status: 401, errors: [I18n.t("api.errors.bad_credentials", default: "Bad Credentials"), exception.message]
16
+ end
17
+
18
+ def unauthorized! exception = StandardError.new
19
+ api_error status: 403, errors: [I18n.t("api.errors.unauthorized", default: "Unauthorized"), exception.message]
20
+ return
21
+ end
22
+
23
+ def not_found! exception = StandardError.new
24
+ return api_error(status: 404, errors: [I18n.t("api.errors.not_found", default: "Not Found"), exception.message])
25
+ end
26
+
27
+ def name_error!
28
+ api_error(status: 501, errors: [I18n.t("api.errors.name_error", default: "Name Error")])
29
+ end
30
+
31
+ def no_method_error!
32
+ api_error(status: 501, errors: [I18n.t("api.errors.no_method_error", default: "No Method Error")])
33
+ end
34
+
35
+ def invalid! exception = StandardError.new
36
+ api_error status: 422, errors: exception.record.errors
37
+ end
38
+
39
+ def fivehundred! exception = StandardError.new
40
+ api_error status: 500, errors: [I18n.t("api.errors.fivehundred", default: "Internal Server Error"), exception.message]
41
+ end
42
+
43
+ def api_error(status: 500, errors: [])
44
+ # puts errors.full_messages if !Rails.env.production? && errors.respond_to?(:full_messages)
45
+ head status && return if errors.empty?
46
+
47
+ # For retrocompatibility, I try to send back only strings, as errors
48
+ errors_response = if errors.respond_to?(:full_messages)
49
+ # Validation Errors
50
+ errors.full_messages.join(", ")
51
+ elsif errors.respond_to?(:error)
52
+ # Generic uncatched error
53
+ errors.error
54
+ elsif errors.respond_to?(:exception)
55
+ # Generic uncatchd error, if the :error property does not exist, exception will
56
+ errors.exception
57
+ elsif errors.is_a? Array
58
+ # An array of values, I like to have them merged
59
+ errors.join(", ")
60
+ else
61
+ # Uncatched Error, comething I don't know, I must return the errors as it is
62
+ errors
63
+ end
64
+ render json: {error: errors_response}, status: status
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ module SchemaBasedApiUser
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ def authenticate password
6
+ self&.valid_password?(password) ? self : nil
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ class JsonWebToken
2
+ class << self
3
+ def encode(payload, expiry = 15.minutes.from_now.to_i)
4
+ JWT.encode(payload.merge(exp: expiry), Rails.application.secrets.secret_key_base)
5
+ end
6
+
7
+ def decode(token)
8
+ body = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
9
+ HashWithIndifferentAccess.new body
10
+ rescue
11
+ nil
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require "schema_based_api/engine"
2
+
3
+ require 'thecore_auth_commons'
4
+ require 'active_hash_relation'
5
+ require 'rack/cors'
6
+ require 'ransack'
7
+ require 'json_web_token'
8
+ require "kaminari"
9
+ require "multi_json"
10
+
11
+ require 'concerns/api_exception_management'
12
+
13
+ module SchemaBasedApi
14
+ # Your code goes here...
15
+ end
@@ -0,0 +1,12 @@
1
+ module SchemaBasedApi
2
+ class Engine < ::Rails::Engine
3
+ # appending migrations to the main app's ones
4
+ initializer :append_migrations do |app|
5
+ unless app.root.to_s.match root.to_s
6
+ config.paths["db/migrate"].expanded.each do |expanded_path|
7
+ app.config.paths["db/migrate"] << expanded_path
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module SchemaBasedApi
2
+ VERSION = '2.1.12'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :schema_based_api do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schema_based_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.12
5
+ platform: ruby
6
+ authors:
7
+ - Gabriele Tassoni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.2
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.0.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.2
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.0.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: thecore_auth_commons
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: jwt
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.2'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: simple_command
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: kaminari
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.2'
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.2'
89
+ - !ruby/object:Gem::Dependency
90
+ name: ransack
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.3'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.3'
103
+ - !ruby/object:Gem::Dependency
104
+ name: active_hash_relation
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.4'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.4'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rack-cors
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.1'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '1.1'
131
+ - !ruby/object:Gem::Dependency
132
+ name: multi_json
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '1.14'
138
+ type: :runtime
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '1.14'
145
+ - !ruby/object:Gem::Dependency
146
+ name: sqlite3
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - ">="
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ description: Ruby on Rails REST APIs built by convention using the DB schema as the
160
+ foundation.
161
+ email:
162
+ - gabriele.tassoni@gmail.com
163
+ executables: []
164
+ extensions: []
165
+ extra_rdoc_files: []
166
+ files:
167
+ - MIT-LICENSE
168
+ - README.md
169
+ - Rakefile
170
+ - app/commands/authenticate_user.rb
171
+ - app/commands/authorize_api_request.rb
172
+ - app/controllers/api/v2/application_controller.rb
173
+ - app/controllers/api/v2/authentication_controller.rb
174
+ - app/controllers/api/v2/info_controller.rb
175
+ - app/controllers/api/v2/users_controller.rb
176
+ - config/initializers/after_initialize_for_schema_based_api.rb
177
+ - config/initializers/cors_api_thecore.rb
178
+ - config/initializers/knock.rb
179
+ - config/initializers/wrap_parameters.rb
180
+ - config/routes.rb
181
+ - lib/concerns/api_exception_management.rb
182
+ - lib/concerns/schema_based_api_user.rb
183
+ - lib/json_web_token.rb
184
+ - lib/schema_based_api.rb
185
+ - lib/schema_based_api/engine.rb
186
+ - lib/schema_based_api/version.rb
187
+ - lib/tasks/schema_based_api_tasks.rake
188
+ homepage: https://github.com/gabrieletassoni/schema_based_api
189
+ licenses:
190
+ - MIT
191
+ metadata: {}
192
+ post_install_message:
193
+ rdoc_options: []
194
+ require_paths:
195
+ - lib
196
+ required_ruby_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ required_rubygems_version: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ requirements: []
207
+ rubygems_version: 3.1.2
208
+ signing_key:
209
+ specification_version: 4
210
+ summary: Convention based RoR engine which uses DB schema introspection to create
211
+ REST APIs.
212
+ test_files: []