API documentation and spec setup
This commit is contained in:
parent
99ecb75c83
commit
900cc91197
7 changed files with 226 additions and 0 deletions
3
Gemfile
3
Gemfile
|
@ -111,4 +111,7 @@ group :test do
|
|||
# code coverage
|
||||
gem 'simplecov', require: false
|
||||
gem 'coveralls', require: false
|
||||
# api
|
||||
gem 'apivore', require: false
|
||||
gem 'hashie', '~> 3.4.6', require: false # https://github.com/westfieldlabs/apivore/issues/114
|
||||
end
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -94,6 +94,13 @@ GEM
|
|||
activerecord (>= 3.0.0)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
apivore (1.6.2)
|
||||
actionpack (>= 4, < 6)
|
||||
hashie (~> 3.3)
|
||||
json-schema (~> 2.5)
|
||||
rspec (~> 3)
|
||||
rspec-expectations (~> 3.1)
|
||||
rspec-mocks (~> 3.1)
|
||||
arel (6.0.4)
|
||||
attribute_normalizer (1.2.0)
|
||||
base32 (0.3.2)
|
||||
|
@ -191,6 +198,7 @@ GEM
|
|||
has_scope (0.7.2)
|
||||
actionpack (>= 4.1)
|
||||
activesupport (>= 4.1)
|
||||
hashie (3.4.6)
|
||||
html2haml (2.2.0)
|
||||
erubis (~> 2.7.0)
|
||||
haml (>= 4.0, < 6)
|
||||
|
@ -217,6 +225,8 @@ GEM
|
|||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.1.0)
|
||||
json-schema (2.8.0)
|
||||
addressable (>= 2.4)
|
||||
jsonapi-renderer (0.2.0)
|
||||
kaminari (1.1.1)
|
||||
activesupport (>= 4.1.0)
|
||||
|
@ -499,6 +509,7 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10.0)
|
||||
acts_as_tree
|
||||
acts_as_versioned!
|
||||
apivore
|
||||
attribute_normalizer
|
||||
better_errors
|
||||
binding_of_caller
|
||||
|
@ -523,6 +534,7 @@ DEPENDENCIES
|
|||
gaffe
|
||||
haml (~> 4.0)
|
||||
haml-rails
|
||||
hashie (~> 3.4.6)
|
||||
i18n-js (~> 3.0.0.rc8)
|
||||
i18n-spec
|
||||
ice_cube
|
||||
|
|
92
doc/API.md
Normal file
92
doc/API.md
Normal file
|
@ -0,0 +1,92 @@
|
|||
# Foodsoft API
|
||||
|
||||
Foodsoft provides a JSON REST API that gives access to operations like
|
||||
like listing open orders, updating the ordergroup's order, and listing financial
|
||||
transactions. Not all Foodsoft functionality is available through the API, but
|
||||
we're open for new additions.
|
||||
|
||||
The API is documented using [Open API 2.0](https://github.com/OAI/OpenAPI-Specification)
|
||||
/ [Swagger](https://swagger.io/) in [swagger.v1.yml](swagger.v1.yml).
|
||||
This provides a machine-readable reference that is used to provide documentation.
|
||||
|
||||
## API endpoint documentation
|
||||
|
||||
>> [View API documentation](http://petstore.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffoodcoops%2Ffoodsoft%2Fmaster%2Fdoc%2Fswagger.v1.yml) <<
|
||||
|
||||
The above documentation can communicate with the API directly on a local development
|
||||
installation of Foodsoft at [http://localhost:3000/f](http://localhost:3000/f).
|
||||
|
||||
You'll need to give access to the application first. This can be done by going to
|
||||
_Administration_ > _Configuration_ > _Apps_ in Foodsoft. Select _New Application_,
|
||||
enter any name, put `http://petstore.swagger.io/oauth2-redirect.html` in _Redirect URI_
|
||||
and disable _Confidential_. After submission, you will have an _Application UID_ that
|
||||
you can enter that as `client_id` after clicking _Authorize_ in the Swagger UI.
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
Uses the [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper) gem,
|
||||
which provides an OAuth2 provider.
|
||||
|
||||
|
||||
### Authorization code flow
|
||||
|
||||
This is the recommended flow for server-side web applications, where
|
||||
members login with Foodsoft, then redirected to the app, which then obtains
|
||||
an access token using the authorization code supplied at redirection.
|
||||
|
||||
Before you can obtain an access token, the client needs to obtain an id and secret.
|
||||
(You can currently skip this for the password credentials flow.) This needs to be
|
||||
done for each Foodsoft scope by an admin.
|
||||
|
||||
1. Click on the _Apps_ button at the right in Foodsoft's configuration screen.
|
||||
2. Click on _New application_
|
||||
3. Enter any _Name_ and put the website of your app in _Redirect URI_ and _Submit_.
|
||||
4. Click on the new applications' name for the app id and secret.
|
||||
5. To quickly test, logging into the app, press _Authorize_.
|
||||
|
||||
Note that the user doesn't need to confirm that he is giving the app access to his
|
||||
Foodsoft account by default, since apps can only be created by admins. If you
|
||||
want to change that, see disable `skip_authorization` in `config/initializers/doorkeeper.rb`.
|
||||
|
||||
[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Authorization-Code-Flow).
|
||||
|
||||
|
||||
### Implicit flow
|
||||
|
||||
This is the recommended flow for client-side web applications. It looks a lot
|
||||
like the authorization code flow, but when redirecting back to the app, the
|
||||
access token is available directly as part of the url _fragment_ (`window.location.hash`).
|
||||
|
||||
This flow also needs to be registered in Foodsoft as in the authorization code flow,
|
||||
but with _Confidential_ disabled. You only need the `client_id`, not the secret.
|
||||
|
||||
**note** please make sure you understand sections
|
||||
[4.4.2](http://tools.ietf.org/html/rfc6819#section-4.4.2) and
|
||||
[4.4.3](http://tools.ietf.org/html/rfc6819#section-4.4.3) of the OAuth2 Threat
|
||||
Model document before using this flow.
|
||||
|
||||
You may find Doorkeeper's [implicit_grant_test](https://github.com/doorkeeper-gem/doorkeeper/blob/master/spec/requests/flows/implicit_grant_spec.rb) useful.
|
||||
|
||||
|
||||
### Password credentials flow
|
||||
|
||||
To obtain a token using a username/password directly, you can do this:
|
||||
|
||||
```ruby
|
||||
require 'oauth2'
|
||||
c = OAuth2::Client.new('client_id', 'secret', site: 'http://localhost:3002/f/', authorize_url: 'oauth/authorize', token_url: 'oauth/token')
|
||||
c.password.get_token('admin', 'secret').token
|
||||
# => "1234567890abcdef1234567890abcdef1234567890abcdef123456790abcdef1"
|
||||
```
|
||||
|
||||
Now use this token as value for the `access_token` when accessing the API, like
|
||||
http://localhost:3002/f/api/v1/financial_transactions/1?access_token=12345...
|
||||
|
||||
[Read more](https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow).
|
||||
|
||||
|
||||
## Logout
|
||||
|
||||
When the user logs out of Foodsoft, all access tokens are destroyed, except when
|
||||
the token's scope includes `offline_access` (so offline applications are possible).
|
67
doc/swagger.v1.yml
Normal file
67
doc/swagger.v1.yml
Normal file
|
@ -0,0 +1,67 @@
|
|||
swagger: '2.0'
|
||||
info:
|
||||
title: Foodsoft API v1
|
||||
version: '1.0.0'
|
||||
description: >
|
||||
[Foodsoft](https://github.com/foodcoops/foodsoft) is web-based software to manage
|
||||
a non-profit food coop (product catalog, ordering, accounting, job scheduling).
|
||||
|
||||
|
||||
This is a description of Foodsoft's API v1.
|
||||
|
||||
|
||||
Note that each food cooperative typically has their own instance (on a shared
|
||||
server or their own installation), and there are just as many APIs (if the Foodsoft
|
||||
version is recent enough).
|
||||
This API description points to the default development url with the default
|
||||
Foodsoft scope - that would be [http://localhost:3000/f](http://localhost:3000/f).
|
||||
externalDocs:
|
||||
description: General Foodsoft API documentation
|
||||
url: https://github.com/foodcoops/foodsoft/blob/master/doc/API.md
|
||||
|
||||
# development url with default scope
|
||||
host: localhost:3000
|
||||
schemes:
|
||||
- 'http'
|
||||
basePath: /f/api/v1
|
||||
|
||||
produces:
|
||||
- 'application/json'
|
||||
|
||||
paths:
|
||||
|
||||
definitions:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: error code
|
||||
error_description:
|
||||
type: string
|
||||
description: human-readable error message (localized)
|
||||
Error404:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: '<tt>not_found</tt>'
|
||||
error_description:
|
||||
$ref: '#/definitions/Error/properties/error_description'
|
||||
Error401:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: '<tt>unauthorized</tt>'
|
||||
error_description:
|
||||
$ref: '#/definitions/Error/properties/error_description'
|
||||
|
||||
securityDefinitions:
|
||||
foodsoft_auth:
|
||||
type: oauth2
|
||||
flow: implicit
|
||||
authorizationUrl: http://localhost:3000/f/oauth/authorize
|
||||
scopes:
|
||||
all: full access to user functions
|
||||
offline_access: retain access after user has logged out
|
19
spec/api/v1/swagger_spec.rb
Normal file
19
spec/api/v1/swagger_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
require 'spec_helper'
|
||||
require 'apivore'
|
||||
|
||||
# we want to load a local file in YAML-format instead of a served JSON file
|
||||
class SwaggerCheckerFile < Apivore::SwaggerChecker
|
||||
def fetch_swagger!
|
||||
YAML.load(File.read(swagger_path))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'API v1', type: :apivore, order: :defined do
|
||||
include ApiHelper
|
||||
|
||||
subject { SwaggerCheckerFile.instance_for Rails.root.join('doc', 'swagger.v1.yml') }
|
||||
|
||||
it 'tests all documented routes' do
|
||||
is_expected.to validate_all_paths
|
||||
end
|
||||
end
|
15
spec/factories/doorkeeper.rb
Normal file
15
spec/factories/doorkeeper.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
require 'factory_bot'
|
||||
require 'doorkeeper'
|
||||
|
||||
FactoryBot.define do
|
||||
|
||||
factory :oauth2_application, class: Doorkeeper::Application do
|
||||
name { Faker::App.name }
|
||||
redirect_uri 'https://example.com:1234/app'
|
||||
end
|
||||
|
||||
factory :oauth2_access_token, class: Doorkeeper::AccessToken do
|
||||
application factory: :oauth2_application
|
||||
end
|
||||
|
||||
end
|
18
spec/support/api_helper.rb
Normal file
18
spec/support/api_helper.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
module ApiHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
let(:user) { create(:user) }
|
||||
let(:api_access_token) { create(:oauth2_access_token, resource_owner_id: user.id).token }
|
||||
let(:api_authorization) { "Bearer #{api_access_token}" }
|
||||
end
|
||||
|
||||
# Add authentication to parameters for {Swagger::RspecHelpers#validate}
|
||||
# @param params [Hash] Query parameters
|
||||
# @return Query parameters with authentication header
|
||||
# @see Swagger::RspecHelpers#validate
|
||||
def api_auth(params = {})
|
||||
{'_headers' => {'Authorization' => api_authorization }}.deep_merge(params)
|
||||
end
|
||||
|
||||
end
|
Loading…
Reference in a new issue