apicasso 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +85 -85
- data/Rakefile +32 -32
- data/app/controllers/apicasso/application_controller.rb +104 -104
- data/app/controllers/apicasso/crud_controller.rb +155 -156
- data/app/controllers/concerns/orderable.rb +44 -44
- data/app/models/apicasso/ability.rb +40 -40
- data/app/models/apicasso/application_record.rb +6 -6
- data/app/models/apicasso/key.rb +25 -25
- data/app/models/apicasso/request.rb +8 -8
- data/config/routes.rb +13 -13
- data/lib/apicasso/engine.rb +6 -6
- data/lib/apicasso/version.rb +3 -3
- data/lib/apicasso.rb +9 -9
- data/lib/generators/apicasso/install/install_generator.rb +25 -25
- data/lib/generators/apicasso/install/templates/create_apicasso_tables.rb +17 -17
- data/lib/tasks/apicasso_tasks.rake +4 -4
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8053255daaad3ecbc6f8b4d14ae1aeb38671b5b9
|
4
|
+
data.tar.gz: d904f5dbc5d8114b26a26620b9b85410fd6d37d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02fcc26644d279ba49a58651e7135e644f2477dcb0d6b01988d27a67bd7a3867e1ff8f298273121b3ccff005225a1b44e0186aa8780bbacb8418369b89ee2a21
|
7
|
+
data.tar.gz: c6c194ff5532e62e6e36d797b143b89520c71fa1b1a470ad1e44343dffede7d740a0cd0a12d190a6bc3125b6030830aea16602513d740f39ebdcd57291344536
|
data/README.md
CHANGED
@@ -1,85 +1,85 @@
|
|
1
|
-
<img src="https://raw.githubusercontent.com/ErvalhouS/APIcasso/master/APIcasso.png" width="300" /> [![Gem Version](https://badge.fury.io/rb/apicasso.svg)](https://badge.fury.io/rb/apicasso) [![Docs Coverage](https://inch-ci.org/github/autoforce/APIcasso.svg?branch=master)](https://inch-ci.org/github/autoforce/APIcasso.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/b58bbd6b9a0376f7cfc8/maintainability)](https://codeclimate.com/github/autoforce/APIcasso/maintainability) [![codecov](https://codecov.io/gh/autoforce/APIcasso/branch/master/graph/badge.svg)](https://codecov.io/gh/autoforce/APIcasso) [![Build Status](https://travis-ci.org/autoforce/APIcasso.svg?branch=master)](https://travis-ci.org/autoforce/APIcasso)
|
2
|
-
|
3
|
-
JSON API development can get boring and time consuming. If you think it through, every time you make one you use almost the same route structure, pointing to the same controller actions, with the same ordering, filtering and pagination features.
|
4
|
-
|
5
|
-
**APIcasso** is intended to be used as a full-fledged CRUD JSON API or as a base controller to speed-up development.
|
6
|
-
It is a route-based resource abstraction using API key scoping. This makes it possible to make CRUD-only applications just by creating functional Rails' models. It is a perfect candidate for legacy Rails projects that do not have an API. Access to your application's resources is managed by a `.scope` JSON object per API key. It uses that permission scope to restrict and extend access.
|
7
|
-
|
8
|
-
## Installation
|
9
|
-
Add this line to your application's `Gemfile`:
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
gem 'apicasso'
|
13
|
-
```
|
14
|
-
|
15
|
-
And then execute this to generate the required migrations:
|
16
|
-
```bash
|
17
|
-
$ rails g apicasso:install
|
18
|
-
```
|
19
|
-
You will need to use a database with JSON fields support to use this gem.
|
20
|
-
|
21
|
-
## Usage
|
22
|
-
After installing APIcasso into your application you can mount a full-fledged CRUD JSON API just by attaching into some route. Usually you will have it under a scoped route like `/api/v1` or a subdomain. You can do that by adding this into your `config/routes.rb`:
|
23
|
-
```ruby
|
24
|
-
# To mount your APIcasso routes under the path scope `/api/v1`
|
25
|
-
mount Apicasso::Engine, at: "/api/v1"
|
26
|
-
# or, if you prefer subdomain scope isolation
|
27
|
-
constraints subdomain: 'apiv1' do
|
28
|
-
mount Apicasso::Engine, at: "/"
|
29
|
-
end
|
30
|
-
```
|
31
|
-
Your API will reflect very similarly a `resources :resource` statement with the following routes:
|
32
|
-
```ruby
|
33
|
-
get '/:resource/' # Index action, listing a `:resource` collection from your application
|
34
|
-
post '/:resource/' # Create action for one `:resource` from your application
|
35
|
-
get '/:resource/:id' # Show action for one `:resource` from your application
|
36
|
-
patch '/:resource/:id' # Update action for one `:resource` from your application
|
37
|
-
delete '/:resource/:id' # Destroy action for one `:resource` from your application
|
38
|
-
get '/:resource/:id/:nested/' # Index action, listing a collection of a `:nested` relation from one of your application's `:resource`
|
39
|
-
options '/:resource/' # A schema dump for the required `:resource`
|
40
|
-
options '/:resource/:id/:nested/' # A schema dump for the required `:nested` relation from one of your application's `:resource`
|
41
|
-
```
|
42
|
-
This means all your application's models will be exposed as `:resource` and it's relations will be exposed as `:nested`. It will enable you to CRUD and get schema metadata from your records.
|
43
|
-
|
44
|
-
> But this is permissive as hell! I do not want to expose my entire application like this, haven't you thought about security?
|
45
|
-
|
46
|
-
*Sure!* The API is being exposed using authentication through `Authorization: Token` [HTTP header authentication](http://tools.ietf.org/html/draft-hammer-http-token-auth-01). The API key objects are manageable through the `Apicasso::Key` model, which gets setup at install. When a new key is created a `.token` is generated using an [Universally Unique Identifier(RFC 4122)](https://tools.ietf.org/html/rfc4122).
|
47
|
-
|
48
|
-
Your API is then exposed based on each `Apicasso::Key.scope` definition
|
49
|
-
```ruby
|
50
|
-
Apicasso::Key.create(scope:
|
51
|
-
{ manage:
|
52
|
-
[{ order: true }, { user: { account_id: 1 } }],
|
53
|
-
read:
|
54
|
-
[{ account: { id: 1 } }]
|
55
|
-
})
|
56
|
-
```
|
57
|
-
This translates directly into which parts of your application is exposed to each APIcasso keys.
|
58
|
-
|
59
|
-
The key from this example will have full access to all orders and to users with `account_id == 1`. It will have also read-only access to accounts with `id == 1`.
|
60
|
-
|
61
|
-
This saves you the trouble of having to setup each and every controller for each model. And even if your application really need it, just make your controllers inherit from `Apicasso::CrudController` and extend it's functionalities. This authorization feature is why one of the dependencies for this gem is [CanCanCan](https://github.com/CanCanCommunity/cancancan), that abstracts the scope field into authorization for your application's resources.
|
62
|
-
|
63
|
-
The `crud#index` and `crud#nested_index` actions are already equipped with pagination, ordering and filtering.
|
64
|
-
|
65
|
-
- You can pass `params[:sort]` with field names preffixed with `+` or `-` to configure custom ordering per request. I.E.: `?sort=+updated_at,-name`
|
66
|
-
- You can pass `params[:q]` using [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) to build a search query. I.E.: `?q[full_name_start]=Picasso`
|
67
|
-
- You can pass `params[:page]` and `params[:per_page]` to build pagination options. I.E.: `?page=2&per_page=12`
|
68
|
-
|
69
|
-
## Contributing
|
70
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/ErvalhouS/APIcasso. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant code of conduct](http://contributor-covenant.org/).
|
71
|
-
|
72
|
-
## License
|
73
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
74
|
-
|
75
|
-
## Code of conduct
|
76
|
-
Everyone interacting in the APIcasso project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ErvalhouS/APIcasso/blob/master/CODE_OF_CONDUCT.md).
|
77
|
-
|
78
|
-
## TODO
|
79
|
-
|
80
|
-
- Abstract a configurable CORS approach.
|
81
|
-
- Add gem options like: Token rotation, Alternative authentication methods
|
82
|
-
- Response fields selecting
|
83
|
-
- Rate limiting
|
84
|
-
- Testing suite
|
85
|
-
- Travis CI
|
1
|
+
<img src="https://raw.githubusercontent.com/ErvalhouS/APIcasso/master/APIcasso.png" width="300" /> [![Gem Version](https://badge.fury.io/rb/apicasso.svg)](https://badge.fury.io/rb/apicasso) [![Docs Coverage](https://inch-ci.org/github/autoforce/APIcasso.svg?branch=master)](https://inch-ci.org/github/autoforce/APIcasso.svg?branch=master) [![Maintainability](https://api.codeclimate.com/v1/badges/b58bbd6b9a0376f7cfc8/maintainability)](https://codeclimate.com/github/autoforce/APIcasso/maintainability) [![codecov](https://codecov.io/gh/autoforce/APIcasso/branch/master/graph/badge.svg)](https://codecov.io/gh/autoforce/APIcasso) [![Build Status](https://travis-ci.org/autoforce/APIcasso.svg?branch=master)](https://travis-ci.org/autoforce/APIcasso)
|
2
|
+
|
3
|
+
JSON API development can get boring and time consuming. If you think it through, every time you make one you use almost the same route structure, pointing to the same controller actions, with the same ordering, filtering and pagination features.
|
4
|
+
|
5
|
+
**APIcasso** is intended to be used as a full-fledged CRUD JSON API or as a base controller to speed-up development.
|
6
|
+
It is a route-based resource abstraction using API key scoping. This makes it possible to make CRUD-only applications just by creating functional Rails' models. It is a perfect candidate for legacy Rails projects that do not have an API. Access to your application's resources is managed by a `.scope` JSON object per API key. It uses that permission scope to restrict and extend access.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
Add this line to your application's `Gemfile`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'apicasso'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute this to generate the required migrations:
|
16
|
+
```bash
|
17
|
+
$ rails g apicasso:install
|
18
|
+
```
|
19
|
+
You will need to use a database with JSON fields support to use this gem.
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
After installing APIcasso into your application you can mount a full-fledged CRUD JSON API just by attaching into some route. Usually you will have it under a scoped route like `/api/v1` or a subdomain. You can do that by adding this into your `config/routes.rb`:
|
23
|
+
```ruby
|
24
|
+
# To mount your APIcasso routes under the path scope `/api/v1`
|
25
|
+
mount Apicasso::Engine, at: "/api/v1"
|
26
|
+
# or, if you prefer subdomain scope isolation
|
27
|
+
constraints subdomain: 'apiv1' do
|
28
|
+
mount Apicasso::Engine, at: "/"
|
29
|
+
end
|
30
|
+
```
|
31
|
+
Your API will reflect very similarly a `resources :resource` statement with the following routes:
|
32
|
+
```ruby
|
33
|
+
get '/:resource/' # Index action, listing a `:resource` collection from your application
|
34
|
+
post '/:resource/' # Create action for one `:resource` from your application
|
35
|
+
get '/:resource/:id' # Show action for one `:resource` from your application
|
36
|
+
patch '/:resource/:id' # Update action for one `:resource` from your application
|
37
|
+
delete '/:resource/:id' # Destroy action for one `:resource` from your application
|
38
|
+
get '/:resource/:id/:nested/' # Index action, listing a collection of a `:nested` relation from one of your application's `:resource`
|
39
|
+
options '/:resource/' # A schema dump for the required `:resource`
|
40
|
+
options '/:resource/:id/:nested/' # A schema dump for the required `:nested` relation from one of your application's `:resource`
|
41
|
+
```
|
42
|
+
This means all your application's models will be exposed as `:resource` and it's relations will be exposed as `:nested`. It will enable you to CRUD and get schema metadata from your records.
|
43
|
+
|
44
|
+
> But this is permissive as hell! I do not want to expose my entire application like this, haven't you thought about security?
|
45
|
+
|
46
|
+
*Sure!* The API is being exposed using authentication through `Authorization: Token` [HTTP header authentication](http://tools.ietf.org/html/draft-hammer-http-token-auth-01). The API key objects are manageable through the `Apicasso::Key` model, which gets setup at install. When a new key is created a `.token` is generated using an [Universally Unique Identifier(RFC 4122)](https://tools.ietf.org/html/rfc4122).
|
47
|
+
|
48
|
+
Your API is then exposed based on each `Apicasso::Key.scope` definition
|
49
|
+
```ruby
|
50
|
+
Apicasso::Key.create(scope:
|
51
|
+
{ manage:
|
52
|
+
[{ order: true }, { user: { account_id: 1 } }],
|
53
|
+
read:
|
54
|
+
[{ account: { id: 1 } }]
|
55
|
+
})
|
56
|
+
```
|
57
|
+
This translates directly into which parts of your application is exposed to each APIcasso keys.
|
58
|
+
|
59
|
+
The key from this example will have full access to all orders and to users with `account_id == 1`. It will have also read-only access to accounts with `id == 1`.
|
60
|
+
|
61
|
+
This saves you the trouble of having to setup each and every controller for each model. And even if your application really need it, just make your controllers inherit from `Apicasso::CrudController` and extend it's functionalities. This authorization feature is why one of the dependencies for this gem is [CanCanCan](https://github.com/CanCanCommunity/cancancan), that abstracts the scope field into authorization for your application's resources.
|
62
|
+
|
63
|
+
The `crud#index` and `crud#nested_index` actions are already equipped with pagination, ordering and filtering.
|
64
|
+
|
65
|
+
- You can pass `params[:sort]` with field names preffixed with `+` or `-` to configure custom ordering per request. I.E.: `?sort=+updated_at,-name`
|
66
|
+
- You can pass `params[:q]` using [ransack's search matchers](https://github.com/activerecord-hackery/ransack#search-matchers) to build a search query. I.E.: `?q[full_name_start]=Picasso`
|
67
|
+
- You can pass `params[:page]` and `params[:per_page]` to build pagination options. I.E.: `?page=2&per_page=12`
|
68
|
+
|
69
|
+
## Contributing
|
70
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ErvalhouS/APIcasso. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant code of conduct](http://contributor-covenant.org/).
|
71
|
+
|
72
|
+
## License
|
73
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
74
|
+
|
75
|
+
## Code of conduct
|
76
|
+
Everyone interacting in the APIcasso project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ErvalhouS/APIcasso/blob/master/CODE_OF_CONDUCT.md).
|
77
|
+
|
78
|
+
## TODO
|
79
|
+
|
80
|
+
- Abstract a configurable CORS approach.
|
81
|
+
- Add gem options like: Token rotation, Alternative authentication methods
|
82
|
+
- Response fields selecting
|
83
|
+
- Rate limiting
|
84
|
+
- Testing suite
|
85
|
+
- Travis CI
|
data/Rakefile
CHANGED
@@ -1,32 +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 = 'Apicasso'
|
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
|
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 = 'Apicasso'
|
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
|
@@ -1,104 +1,104 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# Controller to extract common API features,
|
5
|
-
# such as authentication and authorization
|
6
|
-
class ApplicationController < ActionController::API
|
7
|
-
include ActionController::HttpAuthentication::Token::ControllerMethods
|
8
|
-
prepend_before_action :restrict_access
|
9
|
-
after_action :register_api_request
|
10
|
-
|
11
|
-
# Sets the authorization scope for the current API key
|
12
|
-
def current_ability
|
13
|
-
@current_ability ||= Apicasso::Ability.new(@api_key)
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
# Identifies API key used in the request, avoiding unauthenticated access
|
19
|
-
def restrict_access
|
20
|
-
authenticate_or_request_with_http_token do |token, _options|
|
21
|
-
@api_key = Apicasso::Key.find_by!(token: token)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# Creates a request object in databse, registering the API key and
|
26
|
-
# a hash of the request and the response
|
27
|
-
def register_api_request
|
28
|
-
Apicasso::Request.delay.create(api_key_id: @api_key.id,
|
29
|
-
object: { request: request_hash,
|
30
|
-
response: response_hash })
|
31
|
-
end
|
32
|
-
|
33
|
-
# Request data built as a hash.
|
34
|
-
# Returns UUID, URL, HTTP Headers and origin IP
|
35
|
-
def request_hash
|
36
|
-
{
|
37
|
-
uuid: request.uuid,
|
38
|
-
url: request.original_url,
|
39
|
-
headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
|
40
|
-
ip: request.remote_ip
|
41
|
-
}
|
42
|
-
end
|
43
|
-
|
44
|
-
# Resonse data built as a hash.
|
45
|
-
# Returns HTTP Status and request body
|
46
|
-
def response_hash
|
47
|
-
{
|
48
|
-
status: response.status,
|
49
|
-
body: JSON.parse(response.body)
|
50
|
-
}
|
51
|
-
end
|
52
|
-
|
53
|
-
# Used to avoid errors parsing the search query,
|
54
|
-
# which can be passed as a JSON or as a key-value param
|
55
|
-
def parsed_query
|
56
|
-
JSON.parse(params[:q])
|
57
|
-
rescue JSON::ParserError, TypeError
|
58
|
-
params[:q]
|
59
|
-
end
|
60
|
-
|
61
|
-
# Used to avoid errors in included associations parsing
|
62
|
-
def parsed_include
|
63
|
-
params[:include].split(',')
|
64
|
-
rescue NoMethodError
|
65
|
-
[]
|
66
|
-
end
|
67
|
-
|
68
|
-
# Receives a `.paginate`d collection and returns the pagination
|
69
|
-
# metadata to be merged into response
|
70
|
-
def pagination_metadata_for(records)
|
71
|
-
{ total: records.total_entries,
|
72
|
-
total_pages: records.total_pages,
|
73
|
-
last_page: records.next_page.blank?,
|
74
|
-
previous_page: previous_link_for(records),
|
75
|
-
next_page: next_link_for(records),
|
76
|
-
out_of_bounds: records.out_of_bounds?,
|
77
|
-
offset: records.offset }
|
78
|
-
end
|
79
|
-
|
80
|
-
# Generates a contextualized URL of the next page for this request
|
81
|
-
def next_link_for(records)
|
82
|
-
uri = URI.parse(request.original_url)
|
83
|
-
query = Rack::Utils.parse_query(uri.query)
|
84
|
-
query['page'] = records.next_page
|
85
|
-
uri.query = Rack::Utils.build_query(query)
|
86
|
-
uri.to_s
|
87
|
-
end
|
88
|
-
|
89
|
-
# Generates a contextualized URL of the previous page for this request
|
90
|
-
def previous_link_for(records)
|
91
|
-
uri = URI.parse(request.original_url)
|
92
|
-
query = Rack::Utils.parse_query(uri.query)
|
93
|
-
query['page'] = records.previous_page
|
94
|
-
uri.query = Rack::Utils.build_query(query)
|
95
|
-
uri.to_s
|
96
|
-
end
|
97
|
-
|
98
|
-
# Receives a `:action, :resource, :object` hash to validate authorization
|
99
|
-
def authorize_for(opts = {})
|
100
|
-
authorize! opts[:action], opts[:resource] if opts[:resource].present?
|
101
|
-
authorize! opts[:action], opts[:object] if opts[:object].present?
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Controller to extract common API features,
|
5
|
+
# such as authentication and authorization
|
6
|
+
class ApplicationController < ActionController::API
|
7
|
+
include ActionController::HttpAuthentication::Token::ControllerMethods
|
8
|
+
prepend_before_action :restrict_access
|
9
|
+
after_action :register_api_request
|
10
|
+
|
11
|
+
# Sets the authorization scope for the current API key
|
12
|
+
def current_ability
|
13
|
+
@current_ability ||= Apicasso::Ability.new(@api_key)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Identifies API key used in the request, avoiding unauthenticated access
|
19
|
+
def restrict_access
|
20
|
+
authenticate_or_request_with_http_token do |token, _options|
|
21
|
+
@api_key = Apicasso::Key.find_by!(token: token)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates a request object in databse, registering the API key and
|
26
|
+
# a hash of the request and the response
|
27
|
+
def register_api_request
|
28
|
+
Apicasso::Request.delay.create(api_key_id: @api_key.id,
|
29
|
+
object: { request: request_hash,
|
30
|
+
response: response_hash })
|
31
|
+
end
|
32
|
+
|
33
|
+
# Request data built as a hash.
|
34
|
+
# Returns UUID, URL, HTTP Headers and origin IP
|
35
|
+
def request_hash
|
36
|
+
{
|
37
|
+
uuid: request.uuid,
|
38
|
+
url: request.original_url,
|
39
|
+
headers: request.env.select { |key, _v| key =~ /^HTTP_/ },
|
40
|
+
ip: request.remote_ip
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Resonse data built as a hash.
|
45
|
+
# Returns HTTP Status and request body
|
46
|
+
def response_hash
|
47
|
+
{
|
48
|
+
status: response.status,
|
49
|
+
body: JSON.parse(response.body)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Used to avoid errors parsing the search query,
|
54
|
+
# which can be passed as a JSON or as a key-value param
|
55
|
+
def parsed_query
|
56
|
+
JSON.parse(params[:q])
|
57
|
+
rescue JSON::ParserError, TypeError
|
58
|
+
params[:q]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Used to avoid errors in included associations parsing
|
62
|
+
def parsed_include
|
63
|
+
params[:include].split(',')
|
64
|
+
rescue NoMethodError
|
65
|
+
[]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Receives a `.paginate`d collection and returns the pagination
|
69
|
+
# metadata to be merged into response
|
70
|
+
def pagination_metadata_for(records)
|
71
|
+
{ total: records.total_entries,
|
72
|
+
total_pages: records.total_pages,
|
73
|
+
last_page: records.next_page.blank?,
|
74
|
+
previous_page: previous_link_for(records),
|
75
|
+
next_page: next_link_for(records),
|
76
|
+
out_of_bounds: records.out_of_bounds?,
|
77
|
+
offset: records.offset }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generates a contextualized URL of the next page for this request
|
81
|
+
def next_link_for(records)
|
82
|
+
uri = URI.parse(request.original_url)
|
83
|
+
query = Rack::Utils.parse_query(uri.query)
|
84
|
+
query['page'] = records.next_page
|
85
|
+
uri.query = Rack::Utils.build_query(query)
|
86
|
+
uri.to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
# Generates a contextualized URL of the previous page for this request
|
90
|
+
def previous_link_for(records)
|
91
|
+
uri = URI.parse(request.original_url)
|
92
|
+
query = Rack::Utils.parse_query(uri.query)
|
93
|
+
query['page'] = records.previous_page
|
94
|
+
uri.query = Rack::Utils.build_query(query)
|
95
|
+
uri.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
# Receives a `:action, :resource, :object` hash to validate authorization
|
99
|
+
def authorize_for(opts = {})
|
100
|
+
authorize! opts[:action], opts[:resource] if opts[:resource].present?
|
101
|
+
authorize! opts[:action], opts[:object] if opts[:object].present?
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -1,156 +1,155 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# Controller to consume read-only data to be used on client's frontend
|
5
|
-
class CrudController < Apicasso::ApplicationController
|
6
|
-
before_action :set_root_resource
|
7
|
-
before_action :set_object, except: %i[index schema create]
|
8
|
-
before_action :set_nested_resource, only: %i[nested_index]
|
9
|
-
before_action :set_records, only: %i[index nested_index]
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# `
|
19
|
-
#
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# OPTIONS /:resource
|
72
|
-
#
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
response.headers['Access-Control-Allow-
|
84
|
-
response.headers['Access-Control-Allow-
|
85
|
-
response.headers['Access-Control-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
schemated =
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
#
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Controller to consume read-only data to be used on client's frontend
|
5
|
+
class CrudController < Apicasso::ApplicationController
|
6
|
+
before_action :set_root_resource
|
7
|
+
before_action :set_object, except: %i[index schema create]
|
8
|
+
before_action :set_nested_resource, only: %i[nested_index]
|
9
|
+
before_action :set_records, only: %i[index nested_index]
|
10
|
+
|
11
|
+
include Orderable
|
12
|
+
|
13
|
+
# GET /:resource
|
14
|
+
# Returns a paginated, ordered and filtered query based response.
|
15
|
+
# Consider this
|
16
|
+
# To get all `Channel` sorted by ascending `name` and descending
|
17
|
+
# `updated_at`, filtered by the ones that have a `domain` that matches
|
18
|
+
# exactly `"domain.com"`, paginating records 42 per page and retrieving
|
19
|
+
# the page 42 of that collection. Usage:
|
20
|
+
# GET /sites?sort=+name,-updated_at&q[domain_eq]=domain.com&page=42&per_page=42
|
21
|
+
def index
|
22
|
+
render json: response_json
|
23
|
+
end
|
24
|
+
|
25
|
+
# GET /:resource/1
|
26
|
+
def show
|
27
|
+
render json: @object.to_json(include: parsed_include)
|
28
|
+
end
|
29
|
+
|
30
|
+
# PATCH/PUT /:resource/1
|
31
|
+
def update
|
32
|
+
authorize_for(action: :update,
|
33
|
+
resource: resource.name.underscore.to_sym,
|
34
|
+
object: @object)
|
35
|
+
if @object.update(object_params)
|
36
|
+
render json: @object
|
37
|
+
else
|
38
|
+
render json: @object.errors, status: :unprocessable_entity
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# DELETE /:resource/1
|
43
|
+
def destroy
|
44
|
+
authorize_for(action: :destroy,
|
45
|
+
resource: resource.name.underscore.to_sym,
|
46
|
+
object: @object)
|
47
|
+
if @object.destroy
|
48
|
+
head :no_content
|
49
|
+
else
|
50
|
+
render json: @object.errors, status: :unprocessable_entity
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# GET /:resource/1/:nested_resource
|
55
|
+
alias nested_index index
|
56
|
+
|
57
|
+
# POST /:resource
|
58
|
+
def create
|
59
|
+
@object = resource.new(resource_params)
|
60
|
+
authorize_for(action: :create,
|
61
|
+
resource: resource.name.underscore.to_sym,
|
62
|
+
object: @object)
|
63
|
+
if @object.save
|
64
|
+
render json: @object, status: :created, location: @object
|
65
|
+
else
|
66
|
+
render json: @object.errors, status: :unprocessable_entity
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# OPTIONS /:resource
|
71
|
+
# OPTIONS /:resource/1/:nested_resource
|
72
|
+
# Will return a JSON with the schema of the current resource, using
|
73
|
+
# attribute names as keys and attirbute types as values.
|
74
|
+
def schema
|
75
|
+
set_access_control_headers
|
76
|
+
render json: resource_schema.to_json
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def set_access_control_headers
|
82
|
+
response.headers['Access-Control-Allow-Origin'] = '*'
|
83
|
+
response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
84
|
+
response.headers['Access-Control-Allow-Headers'] = 'Origin, Content-Type, Accept, Authorization, Token, Auth-Token, Email, X-User-Token, X-User-Email'
|
85
|
+
response.headers['Access-Control-Max-Age'] = '1728000'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Common setup to stablish which model is the resource of this request
|
89
|
+
def set_root_resource
|
90
|
+
@root_resource = params[:resource].classify.constantize
|
91
|
+
end
|
92
|
+
|
93
|
+
# Common setup to stablish which object this request is querying
|
94
|
+
def set_object
|
95
|
+
id = params[:id]
|
96
|
+
@object = resource.friendly.find(id)
|
97
|
+
rescue NoMethodError
|
98
|
+
@object = resource.find(id)
|
99
|
+
ensure
|
100
|
+
authorize! :read, @object
|
101
|
+
end
|
102
|
+
|
103
|
+
# Setup to stablish the nested model to be queried
|
104
|
+
def set_nested_resource
|
105
|
+
@nested_resource = @object.send(params[:nested].underscore.pluralize)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Reutrns root_resource if nested_resource is not set scoped by permissions
|
109
|
+
def resource
|
110
|
+
(@nested_resource || @root_resource)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Used to setup the resource's schema, mapping attributes and it's types
|
114
|
+
def resource_schema
|
115
|
+
schemated = {}
|
116
|
+
resource.columns_hash.each { |key, value| schemated[key] = value.type }
|
117
|
+
schemated
|
118
|
+
end
|
119
|
+
|
120
|
+
# Used to setup the records from the selected resource that are
|
121
|
+
# going to be rendered, if authorized
|
122
|
+
def set_records
|
123
|
+
authorize! :read, resource.name.underscore.to_sym
|
124
|
+
@records = resource.ransack(parsed_query).result
|
125
|
+
reorder_records if params[:sort].present?
|
126
|
+
end
|
127
|
+
|
128
|
+
# Reordering of records which happens when receiving `params[:sort]`
|
129
|
+
def reorder_records
|
130
|
+
@records = @records.unscope(:order).order(ordering_params(params))
|
131
|
+
end
|
132
|
+
|
133
|
+
# Raw paginated records object
|
134
|
+
def paginated_records
|
135
|
+
@records.accessible_by(current_ability)
|
136
|
+
.paginate(page: params[:page], per_page: params[:per_page])
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parsing of `paginated_records` with pagination variables metadata
|
140
|
+
def response_json
|
141
|
+
{ entries: entries_json }.merge(pagination_metadata_for(paginated_records))
|
142
|
+
end
|
143
|
+
|
144
|
+
# Parsed JSON to be used as response payload
|
145
|
+
def entries_json
|
146
|
+
JSON.parse(paginated_records.to_json(include: parsed_include))
|
147
|
+
end
|
148
|
+
|
149
|
+
# Only allow a trusted parameter "white list" through,
|
150
|
+
# based on resource's schema.
|
151
|
+
def object_params
|
152
|
+
params.fetch(resource.name.underscore.to_sym, resource_schema.keys)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -1,44 +1,44 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# This concern is used to provide abstract ordering based on `params[:sort]`
|
4
|
-
module Orderable
|
5
|
-
extend ActiveSupport::Concern
|
6
|
-
SORT_ORDER = { '+' => :asc, '-' => :desc }.freeze
|
7
|
-
|
8
|
-
# A list of the param names that can be used for ordering the model list
|
9
|
-
def ordering_params(params)
|
10
|
-
# For example it retrieves a list of orders in descending order of total_value.
|
11
|
-
# Within a specific total_value, older orders are ordered first
|
12
|
-
#
|
13
|
-
# GET /orders?sort=-total_value,created_at
|
14
|
-
# ordering_params(params) # => { total_value: :desc, created_at: :asc }
|
15
|
-
#
|
16
|
-
# Usage:
|
17
|
-
# Order.order(ordering_params(params))
|
18
|
-
ordering = {}
|
19
|
-
params[:sort].try(:split, ',').try(:each) do |attr|
|
20
|
-
parsed_attr = parse_attr attr
|
21
|
-
if model.attribute_names.include?(parsed_attr)
|
22
|
-
ordering[parsed_attr] = SORT_ORDER[parse_sign attr]
|
23
|
-
end
|
24
|
-
end
|
25
|
-
ordering
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
# Parsing of attributes to avoid empty starts in case browser passes "+" as " "
|
31
|
-
def parse_attr(attr)
|
32
|
-
return attr.gsub(/^\ (.*)/, '\1') if attr.starts_with?(' ')
|
33
|
-
attr[1..-1]
|
34
|
-
end
|
35
|
-
|
36
|
-
# Ordering sign parse, which separates
|
37
|
-
def parse_sign(attr)
|
38
|
-
attr =~ /\A[+-]/ ? attr.slice!(0) : '+'
|
39
|
-
end
|
40
|
-
|
41
|
-
def model
|
42
|
-
(params[:resource] || params[:nested] || controller_name).classify.constantize
|
43
|
-
end
|
44
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This concern is used to provide abstract ordering based on `params[:sort]`
|
4
|
+
module Orderable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
SORT_ORDER = { '+' => :asc, '-' => :desc }.freeze
|
7
|
+
|
8
|
+
# A list of the param names that can be used for ordering the model list
|
9
|
+
def ordering_params(params)
|
10
|
+
# For example it retrieves a list of orders in descending order of total_value.
|
11
|
+
# Within a specific total_value, older orders are ordered first
|
12
|
+
#
|
13
|
+
# GET /orders?sort=-total_value,created_at
|
14
|
+
# ordering_params(params) # => { total_value: :desc, created_at: :asc }
|
15
|
+
#
|
16
|
+
# Usage:
|
17
|
+
# Order.order(ordering_params(params))
|
18
|
+
ordering = {}
|
19
|
+
params[:sort].try(:split, ',').try(:each) do |attr|
|
20
|
+
parsed_attr = parse_attr attr
|
21
|
+
if model.attribute_names.include?(parsed_attr)
|
22
|
+
ordering[parsed_attr] = SORT_ORDER[parse_sign attr]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
ordering
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Parsing of attributes to avoid empty starts in case browser passes "+" as " "
|
31
|
+
def parse_attr(attr)
|
32
|
+
return attr.gsub(/^\ (.*)/, '\1') if attr.starts_with?(' ')
|
33
|
+
attr[1..-1]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ordering sign parse, which separates
|
37
|
+
def parse_sign(attr)
|
38
|
+
attr =~ /\A[+-]/ ? attr.slice!(0) : '+'
|
39
|
+
end
|
40
|
+
|
41
|
+
def model
|
42
|
+
(params[:resource] || params[:nested] || controller_name).classify.constantize
|
43
|
+
end
|
44
|
+
end
|
@@ -1,40 +1,40 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# Ability to parse a scope object from Apicasso::Key
|
5
|
-
class Ability
|
6
|
-
include CanCan::Ability
|
7
|
-
|
8
|
-
def initialize(key)
|
9
|
-
key ||= Apicasso::Key.new
|
10
|
-
cannot :manage, :all
|
11
|
-
cannot :read, :all
|
12
|
-
key.scope.each do |permission, klasses_clearances|
|
13
|
-
klasses_clearances.each do |klasses|
|
14
|
-
klasses.each do |klass, clearance|
|
15
|
-
if clearance == true
|
16
|
-
# Usage:
|
17
|
-
# To have a key reading all channels and all accouts
|
18
|
-
# you would have a scope:
|
19
|
-
# => `{read: [{channel: true}, {accout: true}]}`
|
20
|
-
can permission.to_sym, klass.underscore.to_sym
|
21
|
-
can permission.to_sym, klass.classify.constantize
|
22
|
-
elsif clearance.class == Hash
|
23
|
-
# Usage:
|
24
|
-
# To have a key reading all banners from a channel with id 999
|
25
|
-
# you would have a scope:
|
26
|
-
# => `{read: [{banner: {owner_id: [999]}}]}`
|
27
|
-
can permission.to_sym,
|
28
|
-
klass.underscore.to_sym
|
29
|
-
clearance.each do |by_field, values|
|
30
|
-
can permission.to_sym,
|
31
|
-
klass.classify.constantize,
|
32
|
-
by_field => values
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# Ability to parse a scope object from Apicasso::Key
|
5
|
+
class Ability
|
6
|
+
include CanCan::Ability
|
7
|
+
|
8
|
+
def initialize(key)
|
9
|
+
key ||= Apicasso::Key.new
|
10
|
+
cannot :manage, :all
|
11
|
+
cannot :read, :all
|
12
|
+
key.scope.each do |permission, klasses_clearances|
|
13
|
+
klasses_clearances.each do |klasses|
|
14
|
+
klasses.each do |klass, clearance|
|
15
|
+
if clearance == true
|
16
|
+
# Usage:
|
17
|
+
# To have a key reading all channels and all accouts
|
18
|
+
# you would have a scope:
|
19
|
+
# => `{read: [{channel: true}, {accout: true}]}`
|
20
|
+
can permission.to_sym, klass.underscore.to_sym
|
21
|
+
can permission.to_sym, klass.classify.constantize
|
22
|
+
elsif clearance.class == Hash
|
23
|
+
# Usage:
|
24
|
+
# To have a key reading all banners from a channel with id 999
|
25
|
+
# you would have a scope:
|
26
|
+
# => `{read: [{banner: {owner_id: [999]}}]}`
|
27
|
+
can permission.to_sym,
|
28
|
+
klass.underscore.to_sym
|
29
|
+
clearance.each do |by_field, values|
|
30
|
+
can permission.to_sym,
|
31
|
+
klass.classify.constantize,
|
32
|
+
by_field => values
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
|
-
module Apicasso
|
2
|
-
class ApplicationRecord < ActiveRecord::Base
|
3
|
-
self.abstract_class = true
|
4
|
-
self.table_name_prefix = 'apicasso_'
|
5
|
-
end
|
6
|
-
end
|
1
|
+
module Apicasso
|
2
|
+
class ApplicationRecord < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
self.table_name_prefix = 'apicasso_'
|
5
|
+
end
|
6
|
+
end
|
data/app/models/apicasso/key.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'securerandom'
|
4
|
-
module Apicasso
|
5
|
-
# A model to abstract API access, with scope options, token generation, request limiting
|
6
|
-
class Key < ApplicationRecord
|
7
|
-
before_create :set_auth_token
|
8
|
-
|
9
|
-
private
|
10
|
-
|
11
|
-
# Method that generates `SecureRandom.uuid` as token until
|
12
|
-
# an unique one has been acquired
|
13
|
-
def set_auth_token
|
14
|
-
loop do
|
15
|
-
self.token = generate_auth_token
|
16
|
-
break unless self.class.exists?(token: token)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# RFC4122 style token
|
21
|
-
def generate_auth_token
|
22
|
-
SecureRandom.uuid.delete('-')
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
module Apicasso
|
5
|
+
# A model to abstract API access, with scope options, token generation, request limiting
|
6
|
+
class Key < ApplicationRecord
|
7
|
+
before_create :set_auth_token
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
# Method that generates `SecureRandom.uuid` as token until
|
12
|
+
# an unique one has been acquired
|
13
|
+
def set_auth_token
|
14
|
+
loop do
|
15
|
+
self.token = generate_auth_token
|
16
|
+
break unless self.class.exists?(token: token)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# RFC4122 style token
|
21
|
+
def generate_auth_token
|
22
|
+
SecureRandom.uuid.delete('-')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
# A model to abstract API access, with scope options, token generation, request limiting
|
5
|
-
class Request < ApplicationRecord
|
6
|
-
belongs_to :api_key, class_name: 'Apicasso::Key'
|
7
|
-
end
|
8
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
# A model to abstract API access, with scope options, token generation, request limiting
|
5
|
+
class Request < ApplicationRecord
|
6
|
+
belongs_to :api_key, class_name: 'Apicasso::Key'
|
7
|
+
end
|
8
|
+
end
|
data/config/routes.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
Apicasso::Engine.routes.draw do
|
2
|
-
scope module: :apicasso do
|
3
|
-
resources :apidocs, only: [:index]
|
4
|
-
get '/:resource/', to: 'crud#index', via: :get
|
5
|
-
match '/:resource/', to: 'crud#create', via: :post
|
6
|
-
get '/:resource/:id', to: 'crud#show', via: :get
|
7
|
-
match '/:resource/:id', to: 'crud#update', via: :patch
|
8
|
-
match '/:resource/:id', to: 'crud#destroy', via: :delete
|
9
|
-
match '/:resource/:id/:nested/', to: 'crud#nested_index', via: :get
|
10
|
-
match '/:resource/', to: 'crud#schema', via: :options
|
11
|
-
match '/:resource/:id/:nested/', to: 'crud#schema', via: :options
|
12
|
-
end
|
13
|
-
end
|
1
|
+
Apicasso::Engine.routes.draw do
|
2
|
+
scope module: :apicasso do
|
3
|
+
resources :apidocs, only: [:index]
|
4
|
+
get '/:resource/', to: 'crud#index', via: :get
|
5
|
+
match '/:resource/', to: 'crud#create', via: :post
|
6
|
+
get '/:resource/:id', to: 'crud#show', via: :get
|
7
|
+
match '/:resource/:id', to: 'crud#update', via: :patch
|
8
|
+
match '/:resource/:id', to: 'crud#destroy', via: :delete
|
9
|
+
match '/:resource/:id/:nested/', to: 'crud#nested_index', via: :get
|
10
|
+
match '/:resource/', to: 'crud#schema', via: :options
|
11
|
+
match '/:resource/:id/:nested/', to: 'crud#schema', via: :options
|
12
|
+
end
|
13
|
+
end
|
data/lib/apicasso/engine.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
class Engine < ::Rails::Engine
|
5
|
-
end
|
6
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
end
|
6
|
+
end
|
data/lib/apicasso/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
module Apicasso
|
2
|
-
VERSION = '0.1.
|
3
|
-
end
|
1
|
+
module Apicasso
|
2
|
+
VERSION = '0.1.6'
|
3
|
+
end
|
data/lib/apicasso.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'apicasso/version'
|
4
|
-
require 'apicasso/engine'
|
5
|
-
require 'apicasso/active_record_extension'
|
6
|
-
|
7
|
-
module Apicasso
|
8
|
-
# Your code goes here...
|
9
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apicasso/version'
|
4
|
+
require 'apicasso/engine'
|
5
|
+
require 'apicasso/active_record_extension'
|
6
|
+
|
7
|
+
module Apicasso
|
8
|
+
# Your code goes here...
|
9
|
+
end
|
@@ -1,25 +1,25 @@
|
|
1
|
-
require 'rails/generators/migration'
|
2
|
-
|
3
|
-
module Apicasso
|
4
|
-
module Generators
|
5
|
-
class InstallGenerator < ::Rails::Generators::Base
|
6
|
-
include Rails::Generators::Migration
|
7
|
-
source_root File.expand_path('../templates', __FILE__)
|
8
|
-
desc 'Add the required migrations to run APIcasso'
|
9
|
-
|
10
|
-
def self.next_migration_number(path)
|
11
|
-
if @prev_migration_nr
|
12
|
-
@prev_migration_nr += 1
|
13
|
-
else
|
14
|
-
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
15
|
-
end
|
16
|
-
@prev_migration_nr.to_s
|
17
|
-
end
|
18
|
-
|
19
|
-
def copy_migrations
|
20
|
-
migration_template 'create_apicasso_tables.rb',
|
21
|
-
'db/migrate/create_apicasso_tables.rb'
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
1
|
+
require 'rails/generators/migration'
|
2
|
+
|
3
|
+
module Apicasso
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
8
|
+
desc 'Add the required migrations to run APIcasso'
|
9
|
+
|
10
|
+
def self.next_migration_number(path)
|
11
|
+
if @prev_migration_nr
|
12
|
+
@prev_migration_nr += 1
|
13
|
+
else
|
14
|
+
@prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
|
15
|
+
end
|
16
|
+
@prev_migration_nr.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def copy_migrations
|
20
|
+
migration_template 'create_apicasso_tables.rb',
|
21
|
+
'db/migrate/create_apicasso_tables.rb'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,17 +1,17 @@
|
|
1
|
-
class CreateApicassoTables < ActiveRecord::Migration[5.0]
|
2
|
-
def change
|
3
|
-
create_table :apicasso_keys, id: :uuid do |t|
|
4
|
-
t.json :scope
|
5
|
-
t.integer :scope_type
|
6
|
-
t.json :request_limiting
|
7
|
-
t.text :token
|
8
|
-
t.datetime :deleted_at
|
9
|
-
t.timestamps null: false
|
10
|
-
end
|
11
|
-
create_table :apicasso_requests, id: :uuid do |t|
|
12
|
-
t.text :api_key_id
|
13
|
-
t.json :object
|
14
|
-
t.timestamps null: false
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
1
|
+
class CreateApicassoTables < ActiveRecord::Migration[5.0]
|
2
|
+
def change
|
3
|
+
create_table :apicasso_keys, id: :uuid do |t|
|
4
|
+
t.json :scope
|
5
|
+
t.integer :scope_type
|
6
|
+
t.json :request_limiting
|
7
|
+
t.text :token
|
8
|
+
t.datetime :deleted_at
|
9
|
+
t.timestamps null: false
|
10
|
+
end
|
11
|
+
create_table :apicasso_requests, id: :uuid do |t|
|
12
|
+
t.text :api_key_id
|
13
|
+
t.json :object
|
14
|
+
t.timestamps null: false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# desc "Explaining what the task does"
|
2
|
-
# task :apicasso do
|
3
|
-
# # Task goes here
|
4
|
-
# end
|
1
|
+
# desc "Explaining what the task does"
|
2
|
+
# task :apicasso do
|
3
|
+
# # Task goes here
|
4
|
+
# end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apicasso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fernando Bellincanta
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cancancan
|
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
133
|
version: '0'
|
134
134
|
requirements: []
|
135
135
|
rubyforge_project:
|
136
|
-
rubygems_version: 2.
|
136
|
+
rubygems_version: 2.6.14
|
137
137
|
signing_key:
|
138
138
|
specification_version: 4
|
139
139
|
summary: An abstract API design as a mountable engine
|