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: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,3 @@ node_modules/
/storage/*
!/storage/.keep
/public/uploads
.rspec_status
38 changes: 0 additions & 38 deletions .rspec_status

This file was deleted.

31 changes: 27 additions & 4 deletions app/controllers/users/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,45 @@ module Users
class SessionsController < Devise::SessionsController
respond_to :json

before_action :set_json_format

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
credentials = sign_in_params
resource = User.find_for_database_authentication(email: credentials[:email])

if resource&.valid_password?(credentials[:password])
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
else
render json: { error: "Invalid Email or password.", code: "unauthorized" }, status: :unauthorized
end
end

def destroy
sign_out(current_user)
head :no_content
end

private

def set_json_format
request.format = :json
end

def sign_in_params
source = params[:user].presence || params

{
email: source[:email].to_s.strip.downcase,
password: source[:password].to_s
}
end
end
end
12 changes: 12 additions & 0 deletions config/initializers/cors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
origins = ENV["CORS_ALLOWED_ORIGINS"]&.split(",") || ["http://localhost:5174"]
origins origins

resource "/users/sign_in",
headers: :any,
methods: %i[get post put patch delete options head],
credentials: false,
max_age: 600

resource "/users/sign_out",
headers: :any,
methods: %i[get post put patch delete options head],
credentials: false,
max_age: 600

resource "/api/*",
headers: :any,
methods: %i[get post put patch delete options head],
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
namespace :api do
namespace :v1 do
resource :me, only: :show, controller: :me
resources :attendance_entries, only: %i[index create update destroy]
resources :attendance_entries, only: %i[index create destroy] do
member { put :update, action: :update }
end
end
end
devise_for :users, controllers: { sessions: 'users/sessions' }
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/attendance_entries_update_destroy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

RSpec.describe 'AttendanceEntries API', type: :request do
path '/api/v1/attendance_entries/{id}' do
patch 'Update an attendance entry' do
put 'Update an attendance entry' do
tags 'AttendanceEntries'
consumes 'application/json'
produces 'application/json'
Expand Down
131 changes: 131 additions & 0 deletions spec/integration/auth_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
require 'swagger_helper'

RSpec.describe 'Auth API', type: :request do
path '/users/sign_in' do
post 'Sign in and receive JWT' do
tags 'Auth'
consumes 'application/json'
produces 'application/json'
parameter name: :user, in: :body, schema: {
type: :object,
properties: {
user: {
type: :object,
properties: {
email: { type: :string, format: :email },
password: { type: :string }
},
required: %w[email password]
}
},
required: ['user']
}

response '200', 'signed in' do
schema type: :object,
properties: {
token: { type: :string },
user: {
type: :object,
properties: {
id: { type: :integer },
email: { type: :string, format: :email }
},
required: %w[id email]
}
},
required: %w[token user]

let(:existing_user) { create(:user, email: 'auth@example.com', password: 'password') }
let(:user) { { user: { email: existing_user.email, password: 'password' } } }

run_test!
end

response '401', 'unauthorized' do
schema type: :object,
properties: {
error: { type: :string },
code: { type: :string, example: 'unauthorized' }
},
required: %w[error code]

let(:user) { { user: { email: 'auth@example.com', password: 'wrong' } } }

run_test!
end
end
end

path '/users/sign_out' do
delete 'Revoke current JWT' do
tags 'Auth'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: false, description: 'Bearer token'

response '204', 'signed out' do
let(:auth_user) { create(:user, email: 'logout@example.com', password: 'password') }
let(:Authorization) do
post '/users/sign_in', params: { user: { email: auth_user.email, password: 'password' } }, as: :json
"Bearer #{response.parsed_body.fetch('token')}"
end

run_test!
end

response '401', 'unauthorized' do
schema type: :object,
properties: {
error: { type: :string },
code: { type: :string, example: 'unauthorized' }
},
required: %w[error code]

let(:Authorization) { nil }

run_test!
end
end
end

path '/api/v1/me' do
get 'Get current authenticated user' do
tags 'Auth'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'

response '200', 'current user' do
schema type: :object,
properties: {
user: {
type: :object,
properties: {
id: { type: :integer },
email: { type: :string, format: :email }
},
required: %w[id email]
}
},
required: ['user']

let(:me_user) { create(:user, email: 'me@example.com') }
let(:Authorization) { "Bearer #{Warden::JWTAuth::UserEncoder.new.call(me_user, :user, nil).first}" }

run_test!
end

response '401', 'unauthorized' do
schema type: :object,
properties: {
error: { type: :string },
code: { type: :string, example: 'unauthorized' }
},
required: %w[error code]

let(:Authorization) { nil }

run_test!
end
end
end
end
18 changes: 9 additions & 9 deletions spec/requests/api/v1/attendance_entries_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,25 @@ def auth_headers(user)
end
end

describe "PATCH /api/v1/attendance_entries/:id" do
describe "PUT /api/v1/attendance_entries/:id" do
let!(:entry) { create(:attendance_entry, user: user, student_name: "Old") }
let!(:other_entry) { create(:attendance_entry, user: other_user) }

it "updates an entry owned by the user" do
patch "/api/v1/attendance_entries/#{entry.id}",
params: { attendance_entry: { student_name: "New" } },
headers: auth_headers(user),
as: :json
put "/api/v1/attendance_entries/#{entry.id}",
params: { attendance_entry: { student_name: "New" } },
headers: auth_headers(user),
as: :json

expect(response).to have_http_status(:ok)
expect(entry.reload.student_name).to eq("New")
end

it "returns 404 when trying to update someone else's entry" do
patch "/api/v1/attendance_entries/#{other_entry.id}",
params: { attendance_entry: { student_name: "Hax" } },
headers: auth_headers(user),
as: :json
put "/api/v1/attendance_entries/#{other_entry.id}",
params: { attendance_entry: { student_name: "Hax" } },
headers: auth_headers(user),
as: :json

expect(response).to have_http_status(:not_found)
end
Expand Down
12 changes: 12 additions & 0 deletions spec/requests/api/v1/sessions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ def sign_in_user(user)
expect(token).to be_present
end

it "accepts a flat payload from the frontend" do
post "/users/sign_in",
params: { email: user.email, password: user.password },
as: :json

expect(response).to have_http_status(:ok)
expect(response.parsed_body).to include(
"token" => a_kind_of(String),
"user" => include("email" => user.email)
)
end

it "returns unauthorized on bad credentials" do
post "/users/sign_in",
params: { user: { email: user.email, password: "wrong" } },
Expand Down
Loading
Loading