Skip to content

Commit

Permalink
Merge pull request #1 from CodiTramuntana/feat/jsonapi-spec
Browse files Browse the repository at this point in the history
[FEATURE] response as jsonapi specification
  • Loading branch information
agustibr committed Jul 11, 2017
2 parents 1b142bb + 748b403 commit bba841d
Show file tree
Hide file tree
Showing 18 changed files with 322 additions and 47 deletions.
174 changes: 171 additions & 3 deletions README.md
Expand Up @@ -53,7 +53,7 @@ bin/rails db:migrate SCOPE=ctws VERSION=0

### Hook the application `User` model with the engine

By default the user model is `User` but you can change it by creating a `ctws.rb` initializer file in `config/initializers` and put this content in it:
By default the user model is `User` but you can change it by creating or editing the `ctws.rb` initializer file in `config/initializers` and put this content in it:

```ruby
Ctws.user_class = "Account"
Expand All @@ -63,6 +63,14 @@ The application `User` model **must have `email` and `password` attributes**.

For `password` validation [`ActiveModel::SecurePassword::InstanceMethodsOnActivation authenticate`](https://apidock.com/rails/v4.2.7/ActiveModel/SecurePassword/InstanceMethodsOnActivation/authenticate) and [`Devise::Models::DatabaseAuthenticatable#valid_password?`](http://www.rubydoc.info/github/plataformatec/devise/Devise%2FModels%2FDatabaseAuthenticatable:valid_password%3F) User instance methods are supported.

### Set the `JWT` expiry time

By default the token expiry time is 24h but you can change it by creating or editing the `ctws.rb` initializer file in `config/initializers` and put this content in it:

```ruby
Ctws.jwt_expiration_time = 24.hours.from_now
```

<!--
Change the app's models so that they know that they are supposed to act like ctws:
Expand All @@ -80,14 +88,174 @@ end
| Endpoint | Functionality | Requires Authentication? |
| -------------------------------------------------- | -----------------------------------------------: | :-----------------------: |
| `GET /ctws/v1/min_app_version` | Get latest minimum app version for all platforms | No |
| `POST /ctws/signup` | Signup | No |
| `POST /ctws/login` | Login | No |
<!--
| `GET /ctws/v1/min_app_versions` | List all min_app_versions | Yes |
| `GET /ctws/v1/min_app_versions/:id ` | Get a min_app_version | Yes |
| `POST /ctws/v1/min_app_versions` | Creates a min_app_version | Yes |
| `PUT /ctws/v1/min_app_versions/:id` | Updates a min_app_version | Yes |
| `DELETE /ctws/v1/min_app_versions/:id` | Delete a min_app_version | Yes |
| `POST /ctws/signup` | Signup | No |
| `POST /ctws/login` | Login | No |
-->

### min_app_version

**request:**

```bash
curl localhost:3000/ws/v1/min_app_version
```

**response:**

```json
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/vnd.api+json; charset=utf-8
ETag: W/"8dcf1379b7ee203a6d72b3c7773d47f4"
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Request-Id: c1924546-0212-45fe-b86a-83ee3a3b2fa4
X-Runtime: 0.003897
X-XSS-Protection: 1; mode=block

{
"data": [
{
"id": 3,
"type": "min_app_version",
"attributes": {
"codename": "Second release",
"description": "Second release Description text",
"min_version": "0.0.2",
"platform": "android",
"store_uri": "htttps://fdsafdsafdsaf.cot",
"updated_at": "2017-06-22T17:53:31.252+02:00"
}
},
{
"type": "min_app_version",
"id": 1,
"attributes": {
"codename": "First Release",
"description": "You need to update your app. You will be redirected to the corresponding store",
"min_version": "0.0.1",
"platform": "ios",
"store_uri": "https://itunes.apple.com/",
"updated_at": "2017-06-21T14:29:59.348+02:00"
}
}
]
}
```
### signup

**request:**

```bash
curl -X POST -F "email=user@example.com" -F "password=123456789" http://localhost:3000/ws/v1/signup
```

**Successful response:**

```json
HTTP/1.1 201 Created
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/vnd.api+json; charset=utf-8
ETag: W/"ab43e77c2d67636c5c0cd707e661c311"
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Request-Id: 75f9d9cc-4ce2-4aed-9349-e995bb122f39
X-Runtime: 1.727068
X-XSS-Protection: 1; mode=block

{
"data": {
"type": "user",
"id": 20,
"attributes": {
"message": "Account created successfully",
"auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyMCwiZXhwIjoxNDk5ODU1MjQ5fQ.H9ljjShWOAv8b9xn9ZLKv-zgmH8xkPe6dkdhH4JrJPw",
"created_at": "2017-07-11T12:27:27.916+02:00"
}
}
}
```

**Error response:**

```json
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache
Content-Type: application/vnd.api+json; charset=utf-8
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Request-Id: b0d91125-446d-4ed3-95cb-4a702ee24289
X-Runtime: 0.091940
X-XSS-Protection: 1; mode=block

{
"errors": {
"message": "Invalid credentials"
}
}
```

### login

**request:**

```bash
curl -X POST -F "email=user@example.com" -F "password=123456789" http://localhost:3000/ws/v1/login
```

**Successful response:**

```json
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: application/vnd.api+json; charset=utf-8
ETag: W/"4e7a5faaf9eb480a7a7dadb734d01da1"
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Request-Id: 565a8015-9087-480c-b5b6-1dbf634c7d83
X-Runtime: 0.278055
X-XSS-Protection: 1; mode=block

{
"data": {
"type": "authentication",
"attributes": {
"message": "Authenticated user successfully",
"auth_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxNiwiZXhwIjoxNDk5ODU2MDgyfQ.FOLNcInu0yxnp_dqVnyzfzGNwKyv_ERoflhW4cvTa60"
}
}
}
```

**Error response:**

```json
HTTP/1.1 401 Unauthorized
Cache-Control: no-cache
Content-Type: application/vnd.api+json; charset=utf-8
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Request-Id: e95e4b63-3a83-409f-bd05-4cf2dea6136a
X-Runtime: 0.015463
X-XSS-Protection: 1; mode=block

{
"errors": {
"message": "Invalid credentials"
}
}
```

## Tests

Expand Down
17 changes: 12 additions & 5 deletions app/auth/ctws/authenticate_user.rb
@@ -1,13 +1,17 @@
module Ctws
class AuthenticateUser
def initialize(email, password)
def initialize(email, password="12346")
@email = email
@password = password
end

# Service entry point
def call
Ctws::JsonWebToken.encode(user_id: user.id) if user

# attrs_hash = {}
# Ctws.jwt_auth_token_attrs.each {|a| attrs_hash.merge!({"user_#{a}": user.try(a)})}
# Ctws::JsonWebToken.encode(attrs_hash) if user
end

private
Expand All @@ -18,12 +22,15 @@ def call
def user
user = Ctws.user_class.find_by(email: email)

# try method of Active Record's has_secure_password or Devise valid_password?
authenticated = user.try(:authenticate, password) || user.try(:valid_password?, password)

if Ctws.user_validate_with_password
# try method of Active Record's has_secure_password or Devise valid_password?
authenticated = user.try(:authenticate, password) || user.try(:valid_password?, password)
elsif !Ctws.user_validate_with_password
authenticated = true
end
return user if user && authenticated
# raise Authentication error if credentials are invalid
raise(Ctws::ExceptionHandler::AuthenticationError, Ctws::Message.invalid_credentials)
end
end
end
end
29 changes: 19 additions & 10 deletions app/controllers/concerns/ctws/exception_handler.rb
Expand Up @@ -2,6 +2,7 @@ module Ctws
# In the case where the record does not exist,
# ActiveRecord will throw an exception ActiveRecord::RecordNotFound.
# We'll rescue from this exception and return a 404 message.
# List of Rails Status Code Symbols http://billpatrianakos.me/blog/2013/10/13/list-of-rails-status-code-symbols

module ExceptionHandler
# provides the more graceful `included` method
Expand All @@ -12,37 +13,45 @@ class AuthenticationError < StandardError; end
class MissingToken < StandardError; end
class InvalidToken < StandardError; end
class ExpiredSignature < StandardError; end

class UnprocessableEntity < StandardError; end
class RoutingError < StandardError; end

included do
# Define custom handlers
rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two
rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request
rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two
rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two
rescue_from ExceptionHandler::UnprocessableEntity, with: :four_twenty_two
rescue_from ExceptionHandler::ExpiredSignature, with: :four_ninety_eight
rescue_from ExceptionHandler::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::RoutingError, with: :not_found

rescue_from ActiveRecord::RecordNotFound do |e|
json_response({ message: e.message }, :not_found)
end

rescue_from ActiveRecord::RecordInvalid do |e|
json_response({ message: e.message }, :unprocessable_entity)
end
end

# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end

# JSON response with message; Status code 401 - Unauthorized
def not_found(e)
json_response({ message: e.message }, :not_found)
end

# JSON response with message; Status code 422 - unprocessable entity
def four_twenty_two(e)
json_response({ message: e.message }, :unprocessable_entity)
end

# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end

# JSON response with message; Status code 498 - Invalid Token
def four_ninety_eight(e)
json_response({ message: e.message }, :invalid_token)
json_response({ message: e.message }, :invalid_token)
end

end
Expand Down
22 changes: 13 additions & 9 deletions app/controllers/concerns/ctws/response.rb
Expand Up @@ -2,20 +2,24 @@ module Ctws
module Response
# responds with JSON and an HTTP status code (200 by default)
# json_response(@todo, :created)
def success? status
def payload? object, status
case status
when :not_found, :unprocessable_entity, :unauthorized, :invalid_token
false
self.errors_payload(object)
else
true
self.data_payload(object)
end
end
def json_response(object, status = :ok)
responds = {
success: self.success?(status),
data: object
}
render json: responds, status: status

def json_response(object = {}, status = :ok)
render json: self.payload?(object, status), status: status
end

def data_payload(object)
{data: object}
end
def errors_payload(object)
{errors: object}
end
end
end
13 changes: 12 additions & 1 deletion app/controllers/ctws/authentication_controller.rb
Expand Up @@ -5,11 +5,22 @@ class AuthenticationController < CtwsController
# return auth token once user is authenticated
def authenticate
auth_token = Ctws::AuthenticateUser.new(auth_params[:email], auth_params[:password]).call
json_response(auth_token: auth_token)
json_response auth_as_jsonapi(auth_token)

end

private

def auth_as_jsonapi auth_token
{
type: controller_name,
attributes: {
message: Ctws::Message.authenticated_user_success,
auth_token: auth_token,
}
}
end

def auth_params
params.permit(:email, :password)
end
Expand Down
6 changes: 5 additions & 1 deletion app/controllers/ctws/ctws_controller.rb
Expand Up @@ -8,8 +8,12 @@ class CtwsController < ApplicationController

# called before every action on controllers
before_action :authorize_request
skip_before_action :authorize_request, only: [:raise_not_found!]
attr_reader :current_user


def raise_not_found!
raise Ctws::ExceptionHandler::RoutingError, ("#{Ctws::Message.unmatched_route(params[:unmatched_route])}")
end
private

# Check for valid request token and return user
Expand Down

0 comments on commit bba841d

Please sign in to comment.