model_driven_api 2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +205 -0
- data/Rakefile +32 -0
- data/app/commands/authenticate_user.rb +32 -0
- data/app/commands/authorize_api_request.rb +33 -0
- data/app/controllers/api/v2/application_controller.rb +154 -0
- data/app/controllers/api/v2/authentication_controller.rb +12 -0
- data/app/controllers/api/v2/info_controller.rb +68 -0
- data/app/controllers/api/v2/users_controller.rb +9 -0
- data/config/initializers/after_initialize_for_model_driven_api.rb +9 -0
- data/config/initializers/cors_api_thecore.rb +11 -0
- data/config/initializers/knock.rb +59 -0
- data/config/initializers/wrap_parameters.rb +14 -0
- data/config/routes.rb +33 -0
- data/lib/concerns/api_exception_management.rb +58 -0
- data/lib/concerns/model_driven_api_role.rb +47 -0
- data/lib/concerns/model_driven_api_user.rb +46 -0
- data/lib/json_web_token.rb +14 -0
- data/lib/model_driven_api/engine.rb +12 -0
- data/lib/model_driven_api/version.rb +3 -0
- data/lib/model_driven_api.rb +23 -0
- data/lib/tasks/model_driven_api_tasks.rake +4 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f796ea92049910892b48c1904c58192bb7a4b0021a0c0beef89c8e6106f7f64b
|
4
|
+
data.tar.gz: 5054f69acc854b47ab9a5825bb950d035882b05ea6285b81592989ab31d29343
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bbea75413aa62fceddd25f003e23fd6ec16a37f7027be2af549e2f565d91fd8ad8ef93dc07e2bb5cb8b8121aeaf276370ec649222dec8b4893c87a815c4e8a73
|
7
|
+
data.tar.gz: 871479573af6861b9c0aaf9d323668aab7f75afc68452501dd9c1d9253a96cdfc90cd74891e46e0273dfa0abd14357260fe79ab0ffebc92a11766baf2fae28b5
|
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,205 @@
|
|
1
|
+
# Model Driven Api
|
2
|
+
I've always been 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, having the software adapt to data layers and from there building up APIs, visualizations, etc. in an automatic way. This is a first step to have a schema driven API or better model drive, based on the underlining database, the data it has to serve and some sane dafults, or conventions. This effort also gives, thanks to meta programming, an insight on the actual schema, via the info API, the translations available and the DSL which can change the way the data is presented, leading to a strong base for automatica built 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 implementations and versions of this engine, so, this works well if the data is relational, this is 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 or engine.
|
9
|
+
|
10
|
+
# v2?
|
11
|
+
|
12
|
+
Yes, this is the second version of such an effort and you can note it from the api calls, which are all under the ```/api/v2``` namespace the [/api/v1](https://github.com/gabrieletassoni/thecore_api) one, was were it all started, many ideas are ported from there, such as the generation of the automatic model based crud actions, as well as custom actions definitions and all the things that make also this gem useful for my daily job were already in place, but it was too coupled with [thecore](https://github.com/gabrieletassoni/thecore)'s [rails_admin](https://github.com/sferik/rails_admin) UI, making it impossible to create a complete UI-less, API only application, out of the box and directly based of the DB schema, with all the bells and whistles I needed (mainly self adapting, data and schema driven API functionalities).
|
13
|
+
So it all began again, making a better thecore_api gem into this model_driven_api gem, more polished, more functional and self contained.
|
14
|
+
|
15
|
+
# Standards Used
|
16
|
+
|
17
|
+
* [JWT](https://github.com/jwt/ruby-jwt) for authentication.
|
18
|
+
* [CanCanCan](https://github.com/CanCanCommunity/cancancan) for authorization.
|
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 automatically add basic crud operations to any AR model in the app.
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
How to use my plugin.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'model_driven_api'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
```bash
|
34
|
+
$ bundle
|
35
|
+
```
|
36
|
+
|
37
|
+
Or install it yourself as:
|
38
|
+
```bash
|
39
|
+
$ gem install model_driven_api
|
40
|
+
```
|
41
|
+
|
42
|
+
Then run the migrations:
|
43
|
+
```bash
|
44
|
+
$ rails db:migrate
|
45
|
+
```
|
46
|
+
|
47
|
+
This will setup a User model, Role model and the HABTM table between the two.
|
48
|
+
|
49
|
+
Then, if you fire up your ```rails server``` you can already get a jwt and perform different operations.
|
50
|
+
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.
|
51
|
+
|
52
|
+
## Consuming the API
|
53
|
+
|
54
|
+
### Getting the Token
|
55
|
+
|
56
|
+
The first thing that must be done by the client is to get a Token using the credentials:
|
57
|
+
|
58
|
+
```bash
|
59
|
+
POST http://localhost:3000/api/v2/authenticate
|
60
|
+
```
|
61
|
+
|
62
|
+
with a POST body like the one below:
|
63
|
+
|
64
|
+
```json
|
65
|
+
{
|
66
|
+
"auth": {
|
67
|
+
"email": "<REPLACE>",
|
68
|
+
"password": "<REPLACE>"
|
69
|
+
}
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
This action will return in the header a *Token* you can use for the following requests.
|
74
|
+
Bear in mind that the *Token* will expire within 15 minutes and that at each succesful request a new token is returned using the same *Token* header, so, at each interaction between client server, just making an authenticated and succesful request, will give you back a way of continuing to make authenticated requests without the extra overhead of an authentication for each one and without having to keep long expiry times for the *Token*.
|
75
|
+
|
76
|
+
### Info API
|
77
|
+
|
78
|
+
The info API **api/v2/info/** can be used to retrieve general information about the REST API:
|
79
|
+
|
80
|
+
#### Version
|
81
|
+
|
82
|
+
By issuing a GET on this api, you will get a response containing the version of the model_driven_api.
|
83
|
+
This is a request which doesn't require authentication, it could be used as a checkpoint for consuming the resources exposed by this engine.
|
84
|
+
|
85
|
+
```bash
|
86
|
+
GET http://localhost:3000/api/v2/info/version
|
87
|
+
```
|
88
|
+
|
89
|
+
Would produce a response body like this one:
|
90
|
+
|
91
|
+
```json
|
92
|
+
{
|
93
|
+
"version": "2.1.14"
|
94
|
+
}
|
95
|
+
```
|
96
|
+
|
97
|
+
#### Roles
|
98
|
+
|
99
|
+
**Authenticated Request** by issuing a GET request to */api/v2/info/roles*:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
GET http://localhost:3000/api/v2/info/roles
|
103
|
+
```
|
104
|
+
|
105
|
+
Something like this can be retrieved:
|
106
|
+
|
107
|
+
```json
|
108
|
+
[
|
109
|
+
{
|
110
|
+
"id": 1,
|
111
|
+
"name": "role-1586521657646",
|
112
|
+
"created_at": "2020-04-10T12:27:38.061Z",
|
113
|
+
"updated_at": "2020-04-10T12:27:38.061Z",
|
114
|
+
"lock_version": 0
|
115
|
+
},
|
116
|
+
{
|
117
|
+
"id": 2,
|
118
|
+
"name": "role-1586522353509",
|
119
|
+
"created_at": "2020-04-10T12:39:14.276Z",
|
120
|
+
"updated_at": "2020-04-10T12:39:14.276Z",
|
121
|
+
"lock_version": 0
|
122
|
+
}
|
123
|
+
]
|
124
|
+
```
|
125
|
+
|
126
|
+
#### Schema
|
127
|
+
|
128
|
+
**Authenticated Request** This action will send back the *authorized* models accessible by the current user at least for the [:read ability](https://github.com/ryanb/cancan/wiki/checking-abilities). The list will also show the field types of the model and the associations.
|
129
|
+
|
130
|
+
By issuing this GET request:
|
131
|
+
|
132
|
+
```bash
|
133
|
+
GET http://localhost:3000/api/v2/info/roles
|
134
|
+
```
|
135
|
+
|
136
|
+
You will get something like:
|
137
|
+
|
138
|
+
```json
|
139
|
+
{
|
140
|
+
"users": {
|
141
|
+
"id": "integer",
|
142
|
+
"email": "string",
|
143
|
+
"encrypted_password": "string",
|
144
|
+
"admin": "boolean",
|
145
|
+
"lock_version": "integer",
|
146
|
+
"associations": {
|
147
|
+
"has_many": [
|
148
|
+
"role_users",
|
149
|
+
"roles"
|
150
|
+
],
|
151
|
+
"belongs_to": []
|
152
|
+
},
|
153
|
+
"methods": null
|
154
|
+
},
|
155
|
+
"role_users": {
|
156
|
+
"id": "integer",
|
157
|
+
"created_at": "datetime",
|
158
|
+
"updated_at": "datetime",
|
159
|
+
"associations": {
|
160
|
+
"has_many": [],
|
161
|
+
"belongs_to": [
|
162
|
+
"user",
|
163
|
+
"role"
|
164
|
+
]
|
165
|
+
},
|
166
|
+
"methods": null
|
167
|
+
},
|
168
|
+
"roles": {
|
169
|
+
"id": "integer",
|
170
|
+
"name": "string",
|
171
|
+
"created_at": "datetime",
|
172
|
+
"updated_at": "datetime",
|
173
|
+
"lock_version": "integer",
|
174
|
+
"associations": {
|
175
|
+
"has_many": [
|
176
|
+
"role_users",
|
177
|
+
"users"
|
178
|
+
],
|
179
|
+
"belongs_to": []
|
180
|
+
},
|
181
|
+
"methods": null
|
182
|
+
}
|
183
|
+
}
|
184
|
+
```
|
185
|
+
|
186
|
+
The *methods* key will list the **custom actions** that can be used in addition to normal CRUD operations, these can be bulk actions and anything that can serve a purpose, usually to simplify the interaction between client and server (i.e. getting in one request the result of a complex computations which usually would be sorted out using more requests). Later on this topic.
|
187
|
+
|
188
|
+
## Testing
|
189
|
+
|
190
|
+
If you want to manually test the API using [Insomnia](https://insomnia.rest/) you can find the chained request in Insomnia v4 json format inside the **test/insomnia** folder.
|
191
|
+
In the next few days, I'll publish also the rspec tests.
|
192
|
+
|
193
|
+
## TODO
|
194
|
+
|
195
|
+
* Integrate a settings gem
|
196
|
+
* Add DSL for users and roles
|
197
|
+
|
198
|
+
## References
|
199
|
+
THanks to all these people for ideas:
|
200
|
+
|
201
|
+
* [Billy Cheng](https://medium.com/@billy.sf.cheng/a-rails-6-application-part-1-api-1ee5ccf7ed01) For a way to have a nice and clean implementation of the JWT on top of Devise.
|
202
|
+
* [Daniel](https://medium.com/@tdaniel/passing-refreshed-jwts-from-rails-api-using-headers-859f1cfe88e9) For a smart way to manage token expiration.
|
203
|
+
|
204
|
+
## License
|
205
|
+
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 = 'ModelDrivenApi'
|
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
|
+
"AuthenticationError"
|
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,154 @@
|
|
1
|
+
class Api::V2::ApplicationController < ActionController::API
|
2
|
+
# Detect Locale from Accept-Language headers
|
3
|
+
include HttpAcceptLanguage::AutoLocale
|
4
|
+
# Actions will be authorized directly in the action
|
5
|
+
include CanCan::ControllerAdditions
|
6
|
+
include ::ApiExceptionManagement
|
7
|
+
|
8
|
+
attr_accessor :current_user
|
9
|
+
|
10
|
+
before_action :authenticate_request
|
11
|
+
before_action :extract_model
|
12
|
+
before_action :find_record, only: [ :show, :destroy, :update ]
|
13
|
+
|
14
|
+
# GET :controller/
|
15
|
+
def index
|
16
|
+
authorize! :index, @model
|
17
|
+
|
18
|
+
# Custom Action
|
19
|
+
status, result = check_for_custom_action
|
20
|
+
return render json: result, status: 200 if status == true
|
21
|
+
|
22
|
+
# Normal Index Action with Ransack querying
|
23
|
+
@q = (@model.column_names.include?("user_id") ? @model.where(user_id: current_user.id) : @model).ransack(@query.presence|| params[:q])
|
24
|
+
@records_all = @q.result(distinct: true)
|
25
|
+
page = (@page.presence || params[:page])
|
26
|
+
per = (@per.presence || params[:per])
|
27
|
+
pages_info = (@pages_info.presence || params[:pages_info])
|
28
|
+
count = (@count.presence || params[:count])
|
29
|
+
# Paging
|
30
|
+
@records = @records_all.page(page).per(per)
|
31
|
+
|
32
|
+
# If there's the keyword pagination_info, then return a pagination info object
|
33
|
+
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?
|
34
|
+
|
35
|
+
# puts "ALL RECORDS FOUND: #{@records_all.inspect}"
|
36
|
+
status = @records_all.blank? ? 404 : 200
|
37
|
+
# puts "If it's asked for page number, then paginate"
|
38
|
+
return render json: MultiJson.dump(@records, json_attrs), status: status if !page.blank? # (@json_attrs || {})
|
39
|
+
#puts "if you ask for count, then return a json object with just the number of objects"
|
40
|
+
return render json: MultiJson.dump({count: @records_all.count}) if !count.blank?
|
41
|
+
#puts "Default"
|
42
|
+
json_out = MultiJson.dump(@records_all, json_attrs)
|
43
|
+
#puts "JSON ATTRS: #{json_attrs}"
|
44
|
+
#puts "JSON OUT: #{json_out}"
|
45
|
+
render json: json_out, status: status #(@json_attrs || {})
|
46
|
+
end
|
47
|
+
|
48
|
+
def show
|
49
|
+
authorize! :show, @record_id
|
50
|
+
|
51
|
+
# Custom Show Action
|
52
|
+
status, result = check_for_custom_action
|
53
|
+
return render json: result, status: 200 if status == true
|
54
|
+
|
55
|
+
# Normal Show
|
56
|
+
result = @record.to_json(json_attrs)
|
57
|
+
render json: result, status: 200
|
58
|
+
end
|
59
|
+
|
60
|
+
def create
|
61
|
+
@record = @model.new(@body)
|
62
|
+
authorize! :create, @record
|
63
|
+
|
64
|
+
# Custom Action
|
65
|
+
status, result = check_for_custom_action
|
66
|
+
return render json: result, status: 200 if status == true
|
67
|
+
|
68
|
+
# Normal Create Action
|
69
|
+
@record.user_id = current_user.id if @model.column_names.include? "user_id"
|
70
|
+
@record.save!
|
71
|
+
render json: @record.to_json(json_attrs), status: 201
|
72
|
+
end
|
73
|
+
|
74
|
+
def update
|
75
|
+
authorize! :update, @record
|
76
|
+
|
77
|
+
# Custom Action
|
78
|
+
status, result = check_for_custom_action
|
79
|
+
return render json: result, status: 200 if status == true
|
80
|
+
|
81
|
+
# Normal Update Action
|
82
|
+
@record.update_attributes!(@body)
|
83
|
+
render json: @record.to_json(json_attrs), status: 200
|
84
|
+
end
|
85
|
+
|
86
|
+
def destroy
|
87
|
+
authorize! :destroy, @record
|
88
|
+
|
89
|
+
# Custom Action
|
90
|
+
status, result = check_for_custom_action
|
91
|
+
return render json: result, status: 200 if status == true
|
92
|
+
|
93
|
+
# Normal Destroy Action
|
94
|
+
return api_error(status: 500) unless @record.destroy
|
95
|
+
head :ok
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def check_for_custom_action
|
101
|
+
## CUSTOM ACTION
|
102
|
+
# [GET|PUT|POST|DELETE] :controller?do=:custom_action
|
103
|
+
# or
|
104
|
+
# [GET|PUT|POST|DELETE] :controller/:id?do=:custom_action
|
105
|
+
unless params[:do].blank?
|
106
|
+
# Poor man's solution to avoid the possibility to
|
107
|
+
# call an unwanted method in the AR Model.
|
108
|
+
resource = "custom_action_#{params[:do]}"
|
109
|
+
raise NoMethodError unless @model.respond_to?(resource)
|
110
|
+
return true, MultiJson.dump(params[:id].blank? ? @model.send(resource, params) : @model.send(resource, params[:id].to_i, params))
|
111
|
+
end
|
112
|
+
# if it's here there is no custom action in the request querystring
|
113
|
+
return false
|
114
|
+
end
|
115
|
+
|
116
|
+
def authenticate_request
|
117
|
+
@current_user = AuthorizeApiRequest.call(request.headers).result
|
118
|
+
return unauthenticated! unless @current_user
|
119
|
+
current_user = @current_user
|
120
|
+
params[:current_user_id] = @current_user.id
|
121
|
+
# Now every time the user fires off a successful GET request,
|
122
|
+
# a new token is generated and passed to them, and the clock resets.
|
123
|
+
response.headers['Token'] = JsonWebToken.encode(user_id: current_user.id)
|
124
|
+
end
|
125
|
+
|
126
|
+
def find_record
|
127
|
+
record_id ||= (params[:path].split("/").second.to_i rescue nil)
|
128
|
+
@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]))
|
129
|
+
return not_found! if @record.blank?
|
130
|
+
end
|
131
|
+
|
132
|
+
def json_attrs
|
133
|
+
((@model.json_attrs.presence || @json_attrs.presence || {}) rescue {})
|
134
|
+
end
|
135
|
+
|
136
|
+
def extract_model
|
137
|
+
# This method is only valid for ActiveRecords
|
138
|
+
# For any other model-less controller, the actions must be
|
139
|
+
# defined in the route, and must exist in the controller definition.
|
140
|
+
# So, if it's not an activerecord, the find model makes no sense at all
|
141
|
+
# thus must return 404.
|
142
|
+
@model = (params[:ctrl].classify.constantize rescue params[:path].split("/").first.classify.constantize rescue controller_path.classify.constantize rescue controller_name.classify.constantize rescue nil)
|
143
|
+
# Getting the body of the request if it exists, it's ok the singular or
|
144
|
+
# plural form, this helps with automatic tests with Insomnia.
|
145
|
+
@body = params[@model.model_name.singular].presence || params[@model.model_name.route_key]
|
146
|
+
# Only ActiveRecords can have this model caputed
|
147
|
+
return not_found! if (!@model.new.is_a? ActiveRecord::Base rescue false)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Nullifying strong params for API
|
151
|
+
def params
|
152
|
+
request.parameters
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,12 @@
|
|
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
|
+
head :ok
|
10
|
+
end
|
11
|
+
end
|
12
|
+
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: ModelDrivenApi::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,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,58 @@
|
|
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 = AuthenticateUser::AccessDenied.new
|
14
|
+
response.headers['WWW-Authenticate'] = "Token realm=Application"
|
15
|
+
return api_error status: 401, errors: exception.message
|
16
|
+
end
|
17
|
+
|
18
|
+
def unauthorized! exception = CanCan::AccessDenied.new
|
19
|
+
return api_error status: 403, errors: exception.message
|
20
|
+
end
|
21
|
+
|
22
|
+
def not_found! exception = StandardError.new
|
23
|
+
return api_error status: 404, errors: exception.message
|
24
|
+
end
|
25
|
+
|
26
|
+
def invalid! exception = StandardError.new
|
27
|
+
return api_error status: 422, errors: exception.record.errors
|
28
|
+
end
|
29
|
+
|
30
|
+
def fivehundred! exception = StandardError.new
|
31
|
+
return api_error status: 500, errors: exception.message
|
32
|
+
end
|
33
|
+
|
34
|
+
def api_error(status: 500, errors: [])
|
35
|
+
# puts errors.full_messages if !Rails.env.production? && errors.respond_to?(:full_messages)
|
36
|
+
head status && return if errors.empty?
|
37
|
+
|
38
|
+
# For retrocompatibility, I try to send back only strings, as errors
|
39
|
+
errors_response = if errors.respond_to?(:full_messages)
|
40
|
+
# Validation Errors
|
41
|
+
errors.full_messages.join(", ")
|
42
|
+
elsif errors.respond_to?(:error)
|
43
|
+
# Generic uncatched error
|
44
|
+
errors.error
|
45
|
+
elsif errors.respond_to?(:exception)
|
46
|
+
# Generic uncatchd error, if the :error property does not exist, exception will
|
47
|
+
errors.exception
|
48
|
+
elsif errors.is_a? Array
|
49
|
+
# An array of values, I like to have them merged
|
50
|
+
errors.join(", ")
|
51
|
+
else
|
52
|
+
# Uncatched Error, comething I don't know, I must return the errors as it is
|
53
|
+
errors
|
54
|
+
end
|
55
|
+
render json: {error: errors_response}, status: status
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ModelDrivenApiRole
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
## DSL (AKA what to show in the returned JSON)
|
6
|
+
# Use @@json_attrs to drive json rendering for
|
7
|
+
# API model responses (index, show and update ones).
|
8
|
+
# For reference:
|
9
|
+
# https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
10
|
+
# The object passed accepts only these keys:
|
11
|
+
# - only: list [] of model field names in symbol notation to be shown in JSON
|
12
|
+
# serialization.
|
13
|
+
# - except: exclude these fields from the JSON serialization, is a list []
|
14
|
+
# of model field names in symbol notation.
|
15
|
+
# - methods: include the result of some methods defined in the model (virtual
|
16
|
+
# fields).
|
17
|
+
# - include: include associated models, it's a list [] of hashes {} which also
|
18
|
+
# accepts the [:only, :except, :methods, :include] keys.
|
19
|
+
cattr_accessor :json_attrs
|
20
|
+
@@json_attrs = ModelDrivenApi.smart_merge((json_attrs || {}), {
|
21
|
+
except: [
|
22
|
+
:lock_version,
|
23
|
+
:created_at,
|
24
|
+
:updated_at
|
25
|
+
],
|
26
|
+
include: [users: {
|
27
|
+
only: [:id]
|
28
|
+
}]
|
29
|
+
})
|
30
|
+
|
31
|
+
## CUSTOM ACTIONS
|
32
|
+
# Here you can add custom actions to be called from the API
|
33
|
+
# The action must return an serializable (JSON) object.
|
34
|
+
|
35
|
+
# Here you can find an example *without* ID, in the API it could be called like this:
|
36
|
+
# GET /api/v2/:controller?do=test¶meter=sample
|
37
|
+
# def self.custom_action_test params=nil
|
38
|
+
# { test: [ :first, :second, :third ], params: params}
|
39
|
+
# end
|
40
|
+
|
41
|
+
# Here you can find an example *with* ID, in the API it could be called like this:
|
42
|
+
# GET /api/v2/:controller/:id?do=test_with_id¶meter=sample
|
43
|
+
# def self.custom_action_test_with_id id=nil, params=nil
|
44
|
+
# { test: [ :first, :second, :third ], id: id, params: params}
|
45
|
+
# end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ModelDrivenApiUser
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
|
6
|
+
## DSL (AKA what to show in the returned JSON)
|
7
|
+
# Use @@json_attrs to drive json rendering for
|
8
|
+
# API model responses (index, show and update ones).
|
9
|
+
# For reference:
|
10
|
+
# https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
|
11
|
+
# The object passed accepts only these keys:
|
12
|
+
# - only: list [] of model field names in symbol notation to be shown in JSON
|
13
|
+
# serialization.
|
14
|
+
# - except: exclude these fields from the JSON serialization, is a list []
|
15
|
+
# of model field names in symbol notation.
|
16
|
+
# - methods: include the result of some methods defined in the model (virtual
|
17
|
+
# fields).
|
18
|
+
# - include: include associated models, it's a list [] of hashes {} which also
|
19
|
+
# accepts the [:only, :except, :methods, :include] keys.
|
20
|
+
cattr_accessor :json_attrs
|
21
|
+
@@json_attrs = ModelDrivenApi.smart_merge((json_attrs || {}), {
|
22
|
+
except: [
|
23
|
+
:lock_version,
|
24
|
+
:created_at,
|
25
|
+
:updated_at
|
26
|
+
],
|
27
|
+
include: [:roles]
|
28
|
+
})
|
29
|
+
|
30
|
+
## CUSTOM ACTIONS
|
31
|
+
# Here you can add custom actions to be called from the API
|
32
|
+
# The action must return an serializable (JSON) object.
|
33
|
+
|
34
|
+
# Here you can find an example *without* ID, in the API it could be called like this:
|
35
|
+
# GET /api/v2/:controller?do=test¶meter=sample
|
36
|
+
# def self.custom_action_test params=nil
|
37
|
+
# { test: [ :first, :second, :third ], params: params}
|
38
|
+
# end
|
39
|
+
|
40
|
+
# Here you can find an example *with* ID, in the API it could be called like this:
|
41
|
+
# GET /api/v2/:controller/:id?do=test_with_id¶meter=sample
|
42
|
+
# def self.custom_action_test_with_id id=nil, params=nil
|
43
|
+
# { test: [ :first, :second, :third ], id: id, params: params}
|
44
|
+
# end
|
45
|
+
end
|
46
|
+
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,12 @@
|
|
1
|
+
module ModelDrivenApi
|
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,23 @@
|
|
1
|
+
require 'thecore_backend_commons'
|
2
|
+
require 'rack/cors'
|
3
|
+
require 'ransack'
|
4
|
+
require 'json_web_token'
|
5
|
+
require "kaminari"
|
6
|
+
require "multi_json"
|
7
|
+
require "simple_command"
|
8
|
+
|
9
|
+
require 'concerns/api_exception_management'
|
10
|
+
|
11
|
+
require 'deep_merge/rails_compat'
|
12
|
+
|
13
|
+
require "model_driven_api/engine"
|
14
|
+
|
15
|
+
module ModelDrivenApi
|
16
|
+
def self.smart_merge src, dest
|
17
|
+
src.deeper_merge! dest, {
|
18
|
+
extend_existing_arrays: true,
|
19
|
+
merge_hash_arrays: true
|
20
|
+
}
|
21
|
+
src
|
22
|
+
end
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: model_driven_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.2.4
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Gabriele Tassoni
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-05-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thecore_backend_commons
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jwt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: simple_command
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: kaminari
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.2'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.2'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: ransack
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.3'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rack-cors
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.1'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: multi_json
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.14'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.14'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: deep_merge
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.2'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.2'
|
125
|
+
description: Ruby on Rails REST APIs built by convention using the DB schema as the
|
126
|
+
foundation.
|
127
|
+
email:
|
128
|
+
- gabriele.tassoni@gmail.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- MIT-LICENSE
|
134
|
+
- README.md
|
135
|
+
- Rakefile
|
136
|
+
- app/commands/authenticate_user.rb
|
137
|
+
- app/commands/authorize_api_request.rb
|
138
|
+
- app/controllers/api/v2/application_controller.rb
|
139
|
+
- app/controllers/api/v2/authentication_controller.rb
|
140
|
+
- app/controllers/api/v2/info_controller.rb
|
141
|
+
- app/controllers/api/v2/users_controller.rb
|
142
|
+
- config/initializers/after_initialize_for_model_driven_api.rb
|
143
|
+
- config/initializers/cors_api_thecore.rb
|
144
|
+
- config/initializers/knock.rb
|
145
|
+
- config/initializers/wrap_parameters.rb
|
146
|
+
- config/routes.rb
|
147
|
+
- lib/concerns/api_exception_management.rb
|
148
|
+
- lib/concerns/model_driven_api_role.rb
|
149
|
+
- lib/concerns/model_driven_api_user.rb
|
150
|
+
- lib/json_web_token.rb
|
151
|
+
- lib/model_driven_api.rb
|
152
|
+
- lib/model_driven_api/engine.rb
|
153
|
+
- lib/model_driven_api/version.rb
|
154
|
+
- lib/tasks/model_driven_api_tasks.rake
|
155
|
+
homepage: https://github.com/gabrieletassoni/model_driven_api
|
156
|
+
licenses:
|
157
|
+
- MIT
|
158
|
+
metadata:
|
159
|
+
allowed_push_host: https://rubygems.org
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubygems_version: 3.0.3
|
176
|
+
signing_key:
|
177
|
+
specification_version: 4
|
178
|
+
summary: Convention based RoR engine which uses DB schema introspection to create
|
179
|
+
REST APIs.
|
180
|
+
test_files: []
|