Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*.rbc
capybara-*.html
.rspec
.rspec_status
/db/*.sqlite3
/db/*.sqlite3-journal
/db/*.sqlite3-[0-9]*
Expand Down
65 changes: 35 additions & 30 deletions .rspec_status
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
example_id | status | run_time |
----------------------------------------------------------------------- | ------- | --------------- |
./spec/integration/attendance_entries_get_spec.rb[1:1:1:1:1] | passed | 0.03174 seconds |
./spec/integration/attendance_entries_get_spec.rb[1:1:1:2:1] | passed | 0.00362 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:1:1] | passed | 0.022 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:2:1] | passed | 0.00654 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:3:1] | passed | 0.48501 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:1:1] | passed | 0.01754 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:2:1] | passed | 0.00522 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:3:1] | passed | 0.01125 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:1:1] | passed | 0.01602 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:2:1] | passed | 0.00867 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:3:1] | passed | 0.01458 seconds |
./spec/models/attendance_entry_spec.rb[1:1:1] | passed | 0.0027 seconds |
./spec/models/attendance_entry_spec.rb[1:1:2] | passed | 0.00446 seconds |
./spec/models/attendance_entry_spec.rb[1:1:3:1] | passed | 0.00776 seconds |
./spec/models/attendance_entry_spec.rb[1:1:4] | passed | 0.00786 seconds |
./spec/models/attendance_entry_spec.rb[1:1:5] | passed | 0.00938 seconds |
./spec/models/attendance_entry_spec.rb[1:1:6] | passed | 0.00447 seconds |
./spec/models/attendance_entry_spec.rb[1:1:7] | passed | 0.00162 seconds |
./spec/models/jwt_denylist_spec.rb[1:1] | pending | 0.00001 seconds |
./spec/integration/attendance_entries_get_spec.rb[1:1:1:1:1] | passed | 0.02298 seconds |
./spec/integration/attendance_entries_get_spec.rb[1:1:1:2:1] | passed | 0.00291 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:1:1] | passed | 0.01303 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:2:1] | passed | 0.26575 seconds |
./spec/integration/attendance_entries_post_spec.rb[1:1:1:3:1] | passed | 0.04712 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:1:1] | passed | 0.01255 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:2:1] | passed | 0.00317 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:1:1:3:1] | passed | 0.00913 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:1:1] | passed | 0.01171 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:2:1] | passed | 0.0031 seconds |
./spec/integration/attendance_entries_update_destroy_spec.rb[1:2:1:3:1] | passed | 0.00935 seconds |
./spec/models/attendance_entry_spec.rb[1:1:1] | passed | 0.00203 seconds |
./spec/models/attendance_entry_spec.rb[1:1:2] | passed | 0.004 seconds |
./spec/models/attendance_entry_spec.rb[1:1:3:1] | passed | 0.00623 seconds |
./spec/models/attendance_entry_spec.rb[1:1:4] | passed | 0.00643 seconds |
./spec/models/attendance_entry_spec.rb[1:1:5] | passed | 0.00716 seconds |
./spec/models/attendance_entry_spec.rb[1:1:6] | passed | 0.0016 seconds |
./spec/models/attendance_entry_spec.rb[1:1:7] | passed | 0.00148 seconds |
./spec/models/jwt_denylist_spec.rb[1:1] | pending | 0 seconds |
./spec/models/user_spec.rb[1:1] | pending | 0.00002 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:1:1] | passed | 0.05573 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:1:2] | passed | 0.40941 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:2:1] | passed | 0.02259 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:2:2] | passed | 0.0185 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:3:1] | passed | 0.02146 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:3:2] | passed | 0.01857 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[2:1] | passed | 0.02031 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[2:2] | passed | 0.01355 seconds |
./spec/requests/api/v1/me_spec.rb[1:1:1] | passed | 0.04151 seconds |
./spec/requests/api/v1/me_spec.rb[1:1:2] | passed | 0.30706 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:1] | passed | 0.03383 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:1:1] | passed | 0.02612 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:1:2] | passed | 0.01484 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:2:1] | passed | 0.01972 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:2:2] | passed | 0.01551 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:3:1] | passed | 0.01912 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[1:3:2] | passed | 0.01586 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[2:1] | passed | 0.01864 seconds |
./spec/requests/api/v1/attendance_entries_spec.rb[2:2] | passed | 0.01045 seconds |
./spec/requests/api/v1/me_spec.rb[1:1:1] | passed | 0.00842 seconds |
./spec/requests/api/v1/me_spec.rb[1:1:2] | passed | 0.00357 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:1:1] | passed | 0.01037 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:1:2] | passed | 0.01094 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:1:3] | passed | 0.01727 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:2:1] | passed | 0.01432 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:2:2] | passed | 0.02521 seconds |
./spec/requests/api/v1/sessions_spec.rb[1:2:3] | passed | 0.00403 seconds |
11 changes: 11 additions & 0 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@ module Users
class SessionsController < Devise::SessionsController
respond_to :json

prepend_before_action only: :destroy do
unless warden.authenticated?(:user)
render json: { error: "Unauthorized", code: "unauthorized" }, status: :unauthorized
end
end

def create
resource = warden.authenticate!(auth_options)
sign_in(resource_name, resource)
token, = Warden::JWTAuth::UserEncoder.new.call(resource, :user, nil)
render json: { token: token, user: { id: resource.id, email: resource.email } }, status: :ok
end

def destroy
sign_out(current_user)
head :no_content
end
end
end
26 changes: 26 additions & 0 deletions app/lib/users/failure_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Users
class FailureApp < Devise::FailureApp
def respond
if json_request?
json_error_response
else
super
end
end

private

def json_request?
request.format.json? || request.path.start_with?("/api/", "/users/")
end

def json_error_response
self.status = :unauthorized
self.content_type = "application/json"
self.response_body = {
error: i18n_message || "Unauthorized",
code: "unauthorized"
}.to_json
end
end
end
3 changes: 3 additions & 0 deletions config/initializers/devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@
# warden_config.intercept_401 = false
# warden_config.default_strategies(scope: :user).unshift :some_external_strategy
# end
config.warden do |manager|
manager.failure_app = Users::FailureApp
end

# ==> Mountable engine configurations
# When using Devise inside an engine, let's call it `MyEngine`, and this engine
Expand Down
4 changes: 4 additions & 0 deletions spec/requests/api/v1/me_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
get "/api/v1/me", as: :json

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body).to include(
"error" => a_kind_of(String),
"code" => "unauthorized"
)
end
end
end
82 changes: 69 additions & 13 deletions spec/requests/api/v1/sessions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,81 @@
RSpec.describe "Sessions", type: :request do
let(:user) { create(:user, email: "test@example.com", password: "password") }

it "returns a token and lets an authenticated user create attandance entries" do
def sign_in_user(user)
post "/users/sign_in",
params: { user: { email: user.email, password: user.password } },
as: :json

token =
if response.headers["Authorization"].present?
response.headers["Authorization"].split.last
else
response.parsed_body.fetch("token")
end
if response.headers["Authorization"].present?
response.headers["Authorization"].split.last
else
response.parsed_body.fetch("token")
end
end

expect(token).to be_present
describe "POST /users/sign_in" do
it "returns a token on valid credentials" do
token = sign_in_user(user)
expect(token).to be_present
end

post "/api/v1/attendance_entries",
params: { attendance_entry: { student_name: "Jane Doe", status: "present" } },
headers: { "Authorization" => "Bearer #{token}" },
as: :json
it "returns unauthorized on bad credentials" do
post "/users/sign_in",
params: { user: { email: user.email, password: "wrong" } },
as: :json

expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body).to include(
"error" => a_kind_of(String),
"code" => "unauthorized"
)
end

it "allows an authenticated user to access protected endpoints" do
token = sign_in_user(user)

post "/api/v1/attendance_entries",
params: { attendance_entry: { student_name: "Jane Doe", status: "present" } },
headers: { "Authorization" => "Bearer #{token}" },
as: :json

expect(response).to have_http_status(:created).or have_http_status(:ok)
end
end

describe "DELETE /users/sign_out" do
it "returns no content and revokes the token" do
token = sign_in_user(user)

delete "/users/sign_out",
headers: { "Authorization" => "Bearer #{token}" },
as: :json

expect(response).to have_http_status(:no_content)
end

it "prevents a revoked token from accessing protected endpoints" do
token = sign_in_user(user)

delete "/users/sign_out",
headers: { "Authorization" => "Bearer #{token}" },
as: :json

get "/api/v1/me",
headers: { "Authorization" => "Bearer #{token}" },
as: :json

expect(response).to have_http_status(:unauthorized)
end

it "returns unauthorized when signing out without a token" do
delete "/users/sign_out", as: :json

expect(response).to have_http_status(:created).or have_http_status(:ok)
expect(response).to have_http_status(:unauthorized)
expect(response.parsed_body).to eq(
"error" => "Unauthorized",
"code" => "unauthorized"
)
end
end
end
123 changes: 123 additions & 0 deletions swagger/v1/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,115 @@ info:
title: API V1
version: v1
paths:
"/users/sign_in":
post:
summary: Sign in and receive JWT
tags:
- Auth
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
user:
type: object
properties:
email:
type: string
format: email
password:
type: string
required:
- email
- password
required:
- user
responses:
'200':
description: signed in
content:
application/json:
schema:
type: object
properties:
token:
type: string
user:
type: object
properties:
id:
type: integer
email:
type: string
format: email
required:
- token
- user
'401':
description: unauthorized
content:
application/json:
schema:
"$ref": "#/components/schemas/UnauthorizedError"
"/users/sign_out":
delete:
summary: Revoke current JWT
tags:
- Auth
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'204':
description: signed out
'401':
description: unauthorized
content:
application/json:
schema:
"$ref": "#/components/schemas/UnauthorizedError"
"/api/v1/me":
get:
summary: Get current authenticated user
tags:
- Auth
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: current user
content:
application/json:
schema:
type: object
properties:
user:
type: object
properties:
id:
type: integer
email:
type: string
format: email
required:
- user
'401':
description: unauthorized
content:
application/json:
schema:
"$ref": "#/components/schemas/UnauthorizedError"
"/api/v1/attendance_entries":
get:
summary: List attendance entries
Expand Down Expand Up @@ -146,3 +255,17 @@ servers:
variables:
defaultHost:
default: www.example.com
components:
schemas:
UnauthorizedError:
type: object
properties:
error:
type: string
example: Invalid Email or password.
code:
type: string
example: unauthorized
required:
- error
- code
Loading