Skip to content

Commit

Permalink
added api v1 endpoints for user login
Browse files Browse the repository at this point in the history
  • Loading branch information
kortirso committed Mar 10, 2024
1 parent 261908d commit ec287ca
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## Unreleased
### Added
- url localization
- api v1 endpoints for user login

### Modified
- user navigation
Expand Down
27 changes: 27 additions & 0 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Api
module V1
class BaseController < ApplicationController
protect_from_forgery with: :null_session

private

def page_not_found
render json: { errors: ['Not found'] }, status: :not_found
end

def authentication_error
render json: { errors: [t('controllers.authentication.permission')] }, status: :unauthorized
end

def confirmation_error
render json: { errors: [t('controllers.confirmation.permission')] }, status: :unauthorized
end

def ban_error
render json: { errors: [t('controllers.confirmation.ban')] }, status: :unauthorized
end
end
end
end
42 changes: 42 additions & 0 deletions app/controllers/api/v1/users/access_tokens_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

module Api
module V1
module Users
class AccessTokensController < Api::V1::BaseController
include Deps[generate_token: 'services.auth.generate_token']

skip_before_action :authenticate, only: %i[create]
skip_before_action :check_email_confirmation, only: %i[create]
skip_before_action :check_email_ban, only: %i[create]

before_action :find_user, only: %i[create]
before_action :authenticate_user, only: %i[create]

def create
render json: { access_token: generate_token.call(user: @user)[:result] }, status: :created
end

private

def find_user
@user = User.not_banned.find_by!(email: user_params[:email]&.strip&.downcase)
end

def authenticate_user
return if @user.authenticate(user_params[:password])

failed_sign_in
end

def failed_sign_in
render json: { errors: [t('controllers.users.sessions.invalid')] }, status: :bad_request
end

def user_params
params.require(:user).permit(:email, :password)
end
end
end
end
end
32 changes: 32 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Api
module V1
class UsersController < Api::V1::BaseController
include Deps[
generate_token: 'services.auth.generate_token',
create_form: 'forms.users.create'
]

skip_before_action :authenticate, only: %i[create]
skip_before_action :check_email_confirmation, only: %i[create]
skip_before_action :check_email_ban, only: %i[create]

def create
case create_form.call(params: user_params.to_h.symbolize_keys)
in { errors: errors } then render json: { errors: errors }, status: :bad_request
in { result: result }
render json: { access_token: generate_token.call(user: result)[:result] }, status: :created
end
end

private

def user_params
params_hash = params.require(:user).permit(:email, :password, :password_confirmation)
params_hash[:email] = params_hash[:email].strip.downcase
params_hash
end
end
end
end
3 changes: 2 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class ApplicationController < ActionController::Base
include Parameterable
include Watchable

# TODO: remember to skip redundant before actions in Api::Frontend::BaseController, Admin::BaseController
# TODO: remember to skip redundant before actions in
# Api::Frontend::BaseController, Admin::BaseController, Api::V1::BaseController
before_action :authenticate, except: %i[page_not_found]
before_action :check_email_confirmation, except: %i[page_not_found]
before_action :check_email_ban, except: %i[page_not_found]
Expand Down
10 changes: 9 additions & 1 deletion app/controllers/concerns/confirmation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ module Confirmation
def check_email_confirmation
return if Current.user.nil? || Current.user.confirmed?

redirect_to users_confirm_path, alert: t('controllers.confirmation.permission')
confirmation_error
end

def check_email_ban
return if Current.user.nil? || !Current.user.banned?

ban_error
end

def confirmation_error
redirect_to users_confirm_path, alert: t('controllers.confirmation.permission')
end

def ban_error
redirect_to root_path, alert: t('controllers.confirmation.ban')
end
end
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@

scope '(:locale)', locale: /#{I18n.available_locales.join('|')}/, defaults: { locale: nil } do
namespace :api do
namespace :v1 do
namespace :users do
resource :access_tokens, only: %i[create]
end
resources :users, only: %i[create]
end

namespace :frontend do
resource :notifications, only: %i[create destroy]
resource :feedback, only: %i[create]
Expand Down
76 changes: 76 additions & 0 deletions spec/controllers/api/v1/users/access_tokens_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

describe Api::V1::Users::AccessTokensController do
describe 'POST#create' do
context 'for unexisting user' do
it 'returns error', :aggregate_failures do
post :create, params: { user: { email: '[email protected]', password: '1' } }

expect(response).to have_http_status :not_found
expect(response.parsed_body['access_token']).to be_blank
end
end

context 'for existing user' do
let!(:user) { create :user }

context 'for unconfirmed email' do
before { user.update!(confirmed_at: nil) }

it 'returns access token', :aggregate_failures do
post :create, params: { user: { email: user.email.upcase, password: user.password } }

expect(response).to have_http_status :created
expect(response.parsed_body['access_token']).not_to be_blank
end
end

context 'for invalid password' do
it 'returns error', :aggregate_failures do
post :create, params: { user: { email: user.email, password: 'invalid_password' } }

expect(response).to have_http_status :bad_request
expect(response.parsed_body['access_token']).to be_blank
end
end

context 'for empty password' do
it 'returns error', :aggregate_failures do
post :create, params: { user: { email: user.email, password: '' } }

expect(response).to have_http_status :bad_request
expect(response.parsed_body['access_token']).to be_blank
end
end

context 'for valid password' do
it 'returns access token', :aggregate_failures do
post :create, params: { user: { email: user.email, password: user.password } }

expect(response).to have_http_status :created
expect(response.parsed_body['access_token']).not_to be_blank
end
end

context 'for valid password and upcased email' do
it 'returns access token', :aggregate_failures do
post :create, params: { user: { email: user.email.upcase, password: user.password } }

expect(response).to have_http_status :created
expect(response.parsed_body['access_token']).not_to be_blank
end

context 'for banned user' do
before { user.update!(banned_at: DateTime.now) }

it 'returns error', :aggregate_failures do
post :create, params: { user: { email: user.email.upcase, password: user.password } }

expect(response).to have_http_status :not_found
expect(response.parsed_body['access_token']).to be_blank
end
end
end
end
end
end
65 changes: 65 additions & 0 deletions spec/controllers/api/v1/users_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

describe Api::V1::UsersController do
describe 'POST#create' do
context 'for invalid credentials' do
let(:request) { post :create, params: { user: { email: '', password: '1' } } }

it 'does not create user', :aggregate_failures do
expect { request }.not_to change(User, :count)
expect(response).to have_http_status :bad_request
end
end

context 'for short password' do
let(:request) { post :create, params: { user: { email: '[email protected]', password: '1' } } }

it 'does not create new user', :aggregate_failures do
expect { request }.not_to change(User, :count)
expect(response).to have_http_status :bad_request
end
end

context 'without password confirmation' do
let(:request) { post :create, params: { user: { email: '[email protected]', password: '12345678' } } }

it 'does not create new user', :aggregate_failures do
expect { request }.not_to change(User, :count)
expect(response).to have_http_status :bad_request
end
end

context 'for existing user' do
let!(:user) { create :user }
let(:request) {
post :create, params: { user: { email: user.email, password: '12345678', password_confirmation: '12345678' } }
}

it 'does not create new user', :aggregate_failures do
expect { request }.not_to change(User, :count)
expect(response).to have_http_status :bad_request
end
end

context 'for valid data' do
let(:user_params) { { email: ' [email protected] ', password: '12345678', password_confirmation: '12345678' } }
let(:request) { post :create, params: { user: user_params } }

it 'creates new user', :aggregate_failures do
expect { request }.to change(User, :count).by(1)
expect(User.last.email).to eq '[email protected]'
expect(response).to have_http_status :created
expect(response.parsed_body['access_token']).not_to be_blank
end

context 'for banned email' do
before { create :banned_email, value: '[email protected]' }

it 'does not create new user', :aggregate_failures do
expect { request }.not_to change(User, :count)
expect(response).to have_http_status :bad_request
end
end
end
end
end

0 comments on commit ec287ca

Please sign in to comment.