Skip to content

Commit d2ea59c

Browse files
committed
Update application (versioning, CORS, README)
1 parent fdd5ef2 commit d2ea59c

File tree

13 files changed

+151
-67
lines changed

13 files changed

+151
-67
lines changed

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ This is an example of using GrapeOAuth2 gem with the Grape API project.
66

77
This app is ready to deploy to Heroku [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/grape-oauth2/grape-oauth2-sample).
88

9-
Project stack includes: **Grape, Grape-Entity, Grape-OAuth2, ActiveRecord 5, Puma, PostgreSQL, Dotenv, Rubocop, RSpec**.
9+
Project stack includes: **Grape, Grape::Entity, GrapeOAuth2, ActiveRecord 5, Puma, PostgreSQL, Dotenv, Rack::Cors, Rubocop, RSpec**.
1010

1111
## Implemented features
1212

13-
* API endpoints (GET, POST);
13+
* API endpoints with different types of requests (GET, POST);
14+
* API versioning
1415
* Resource Owner password credentials authentication;
1516
* Protected endpoints access with OAuth Tokens;
1617
* Generate new Access Token via Refresh Token.
@@ -19,7 +20,20 @@ Project stack includes: **Grape, Grape-Entity, Grape-OAuth2, ActiveRecord 5, Pum
1920

2021
To run the application do the following from your command-line:
2122

22-
`> bundle exec rackup config.ru`
23+
`bundle exec rackup config.ru`
24+
25+
Available API:
26+
27+
```
28+
GET | /api(.json) | | Root action
29+
POST | /api/oauth/authorize(.json) | | OAuth 2.0 Authorization Endpoint
30+
POST | /api/oauth/token(.json) | | OAuth 2.0 Token Endpoint
31+
POST | /api/oauth/revoke(.json) | | OAuth 2.0 Token Revocation
32+
GET | /api/:version/me(.json) | v1 | Information about current resource owner
33+
GET | /api/:version/posts(.json) | v1 | Get all the posts without authorization
34+
GET | /api/:version/posts/:id(.json) | v1 | Read post by ID only if it belongs to authorized author
35+
POST | /api/:version/posts(.json) | v1 | Create post from authorized user
36+
```
2337

2438
## Routes
2539

app/api.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ class API < ::Grape::API
55

66
include GrapeOAuth2.api
77

8-
mount ::GrapeOAuth2Sample::Endpoints::Posts
9-
10-
desc 'Root'
8+
desc 'Root action'
119

1210
get '/' do
1311
{ error: ['Please check API documentation'] }
1412
end
13+
14+
mount ::GrapeOAuth2Sample::V1::Base
1515
end
1616
end

app/endpoints/posts.rb

Lines changed: 0 additions & 41 deletions
This file was deleted.

app/entities/post.rb

Lines changed: 0 additions & 9 deletions
This file was deleted.

app/v1/base.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module GrapeOAuth2Sample
2+
module V1
3+
class Base < ::Grape::API
4+
version 'v1', using: :path
5+
6+
mount ::GrapeOAuth2Sample::V1::Endpoints::Posts
7+
8+
desc 'Information about current resource owner'
9+
10+
get '/me' do
11+
access_token_required!
12+
13+
present(current_resource_owner, with: Entities::User)
14+
end
15+
end
16+
end
17+
end

app/v1/endpoints/posts.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module GrapeOAuth2Sample
2+
module V1
3+
module Endpoints
4+
class Posts < Grape::API
5+
resources :posts do
6+
desc 'Get all the posts without authorization'
7+
8+
get '/' do
9+
posts = Post.take(20)
10+
present(posts, with: GrapeOAuth2Sample::V1::Entities::Post)
11+
end
12+
13+
desc 'Read post by ID only if it belongs to authorized author'
14+
15+
params do
16+
requires :id, type: Integer, desc: 'ID of the post'
17+
end
18+
19+
get ':id', scopes: [:read] do
20+
access_token_required!
21+
22+
post = current_resource_owner.posts.find(params[:id])
23+
present(post, with: GrapeOAuth2Sample::V1::Entities::Post)
24+
end
25+
26+
desc 'Create post from authorized user'
27+
28+
params do
29+
requires :name, type: String, desc: 'Post name'
30+
requires :body, type: String, desc: 'Post body'
31+
end
32+
33+
post '/', scopes: [:write] do
34+
access_token_required!
35+
36+
post = current_resource_owner.posts.create!(declared(params))
37+
present(post, with: GrapeOAuth2Sample::V1::Entities::Post)
38+
end
39+
end
40+
end
41+
end
42+
end
43+
end

app/v1/entities/post.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module GrapeOAuth2Sample
2+
module V1
3+
module Entities
4+
class Post < Grape::Entity
5+
expose :id
6+
expose :name
7+
expose :body
8+
end
9+
end
10+
end
11+
end

app/v1/entities/user.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module GrapeOAuth2Sample
2+
module V1
3+
module Entities
4+
class User < Grape::Entity
5+
expose :id
6+
expose :username
7+
end
8+
end
9+
end
10+
end

config.ru

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
22

33
require 'config/application'
44

5+
use Rack::Cors do
6+
allow do
7+
origins '*'
8+
resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
9+
end
10+
end
11+
512
use OTR::ActiveRecord::ConnectionManagement
613
run GrapeOAuth2Sample::API

config/application.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
require 'dotenv'
55
Dotenv.load(File.expand_path('.env.local'), File.expand_path('.env'))
66

7+
require 'rack/cors'
8+
79
require 'otr-activerecord'
810
require 'grape'
911
require 'grape-entity'
@@ -25,13 +27,16 @@
2527
end
2628

2729
# Entities
28-
Dir[File.expand_path('../../app/entities/*.rb', __FILE__)].each do |entity|
30+
Dir[File.expand_path('../../app/v1/entities/*.rb', __FILE__)].each do |entity|
2931
require_relative entity
3032
end
3133

3234
# Endpoints
33-
Dir[File.expand_path('../../app/endpoints/*.rb', __FILE__)].each do |endpoint|
35+
Dir[File.expand_path('../../app/v1/endpoints/*.rb', __FILE__)].each do |endpoint|
3436
require_relative endpoint
3537
end
3638

39+
# Versioned APIs
40+
require_relative '../app/v1/base'
41+
3742
require_relative '../app/api'

config/database.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
OTR::ActiveRecord.configure_from_file!(File.expand_path('../database.yml', __FILE__))
33

44
env = ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
5+
log_file = File.expand_path("../../log/#{env}.log", __FILE__)
56

67
::ActiveRecord::Base.default_timezone = :utc
7-
::ActiveRecord::Base.logger = Logger.new(File.expand_path("../../log/#{env}.log", __FILE__), File::WRONLY | File::APPEND)
8+
::ActiveRecord::Base.logger = Logger.new(log_file, File::WRONLY | File::APPEND)
89

910
::ActiveRecord::Migration.verbose = false

spec/requests/v1/me_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'spec_helper'
2+
3+
describe 'Me' do
4+
let(:application) { Application.create(name: 'App1') }
5+
let(:user) { User.create(username: 'Jack Sparrow', password: '12345678') }
6+
7+
context 'with access token' do
8+
it 'returns data without authorization for public route' do
9+
access_token = AccessToken.create_for(application, user)
10+
11+
get 'api/v1/me', access_token: access_token.token
12+
13+
expect(last_response.status).to eq 200
14+
expect(json_body).to include(:id, :username)
15+
end
16+
end
17+
18+
context 'without access token' do
19+
it 'returns data without authorization for public route' do
20+
get 'api/v1/me'
21+
22+
expect(last_response.status).to eq 401
23+
expect(json_body[:error]).to eq('unauthorized')
24+
end
25+
end
26+
end

spec/requests/posts_spec.rb renamed to spec/requests/v1/posts_spec.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
Post.create(name: "post ##{index}", body: 'Body')
1313
end
1414

15-
get 'api/posts'
15+
get 'api/v1/posts'
1616

1717
expect(last_response.status).to eq 200
1818
expect(json_body.size).to eq(2)
@@ -21,7 +21,7 @@
2121

2222
context 'protected endpoints' do
2323
it 'returns Unauthorized without token' do
24-
get "api/posts/#{user_post.id}"
24+
get "api/v1/posts/#{user_post.id}"
2525

2626
expect(last_response.status).to eq 401
2727
expect(json_body[:error]).to eq('unauthorized')
@@ -30,7 +30,7 @@
3030
it 'returns Forbidden with token that does not has required scopes' do
3131
access_token = AccessToken.create_for(application, user)
3232

33-
get "api/posts/#{user_post.id}", access_token: access_token.token
33+
get "api/v1/posts/#{user_post.id}", access_token: access_token.token
3434

3535
expect(last_response.status).to eq 403
3636
expect(json_body[:error]).to eq('forbidden')
@@ -40,7 +40,7 @@
4040
access_token = AccessToken.create_for(application, user, 'read')
4141
access_token.update(expires_at: (Time.now - 24000).utc)
4242

43-
get "api/posts/#{user_post.id}", access_token: access_token.token
43+
get "api/v1/posts/#{user_post.id}", access_token: access_token.token
4444

4545
expect(last_response.status).to eq 403
4646
expect(json_body[:error]).to eq('forbidden')
@@ -50,7 +50,7 @@
5050
access_token = AccessToken.create_for(application, user, 'read')
5151
access_token.update(revoked_at: (Time.now - 24000).utc)
5252

53-
get "api/posts/#{user_post.id}", access_token: access_token.token
53+
get "api/v1/posts/#{user_post.id}", access_token: access_token.token
5454

5555
expect(last_response.status).to eq 403
5656
expect(json_body[:error]).to eq('forbidden')
@@ -59,7 +59,7 @@
5959
it 'returns posts with valid token' do
6060
access_token = AccessToken.create_for(application, user, 'read')
6161

62-
get "api/posts/#{user_post.id}", access_token: access_token.token
62+
get "api/v1/posts/#{user_post.id}", access_token: access_token.token
6363

6464
expect(last_response.status).to eq 200
6565
expect(json_body).to include(:id, :name, :body)
@@ -68,7 +68,7 @@
6868
it 'does not creates a Post with invalid token' do
6969
access_token = AccessToken.create_for(application, user, 'read')
7070

71-
post 'api/posts', name: 'Super post', body: 'Body', access_token: access_token.token
71+
post 'api/v1/posts', name: 'Super post', body: 'Body', access_token: access_token.token
7272

7373
expect(last_response.status).to eq 403
7474
expect(Post.count).to be_zero
@@ -78,7 +78,7 @@
7878
access_token = AccessToken.create_for(application, user, 'read write')
7979

8080
expect {
81-
post 'api/posts', name: 'Super post', body: 'Body', access_token: access_token.token
81+
post 'api/v1/posts', name: 'Super post', body: 'Body', access_token: access_token.token
8282
}.to change { Post.count }.from(0).to(1)
8383

8484
expect(last_response.status).to eq 201

0 commit comments

Comments
 (0)