From ec287ca0bdde58ed559a8e7a635c64c249cc8b13 Mon Sep 17 00:00:00 2001 From: Bogdanov Anton Date: Sun, 10 Mar 2024 13:45:30 +0300 Subject: [PATCH] added api v1 endpoints for user login --- CHANGELOG.md | 1 + app/controllers/api/v1/base_controller.rb | 27 +++++++ .../api/v1/users/access_tokens_controller.rb | 42 ++++++++++ app/controllers/api/v1/users_controller.rb | 32 ++++++++ app/controllers/application_controller.rb | 3 +- app/controllers/concerns/confirmation.rb | 10 ++- config/routes.rb | 7 ++ .../v1/users/access_tokens_controller_spec.rb | 76 +++++++++++++++++++ .../api/v1/users_controller_spec.rb | 65 ++++++++++++++++ 9 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/base_controller.rb create mode 100644 app/controllers/api/v1/users/access_tokens_controller.rb create mode 100644 app/controllers/api/v1/users_controller.rb create mode 100644 spec/controllers/api/v1/users/access_tokens_controller_spec.rb create mode 100644 spec/controllers/api/v1/users_controller_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba54849..1e0a72c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb new file mode 100644 index 00000000..8905207e --- /dev/null +++ b/app/controllers/api/v1/base_controller.rb @@ -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 diff --git a/app/controllers/api/v1/users/access_tokens_controller.rb b/app/controllers/api/v1/users/access_tokens_controller.rb new file mode 100644 index 00000000..3698ae89 --- /dev/null +++ b/app/controllers/api/v1/users/access_tokens_controller.rb @@ -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 diff --git a/app/controllers/api/v1/users_controller.rb b/app/controllers/api/v1/users_controller.rb new file mode 100644 index 00000000..c834af42 --- /dev/null +++ b/app/controllers/api/v1/users_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8e971ccb..f9f403f1 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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] diff --git a/app/controllers/concerns/confirmation.rb b/app/controllers/concerns/confirmation.rb index 4611a6f1..897c60c4 100644 --- a/app/controllers/concerns/confirmation.rb +++ b/app/controllers/concerns/confirmation.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 46f061d8..f9d9c9d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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] diff --git a/spec/controllers/api/v1/users/access_tokens_controller_spec.rb b/spec/controllers/api/v1/users/access_tokens_controller_spec.rb new file mode 100644 index 00000000..a05c82c0 --- /dev/null +++ b/spec/controllers/api/v1/users/access_tokens_controller_spec.rb @@ -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: 'unexisting@gmail.com', 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 diff --git a/spec/controllers/api/v1/users_controller_spec.rb b/spec/controllers/api/v1/users_controller_spec.rb new file mode 100644 index 00000000..0d16dd10 --- /dev/null +++ b/spec/controllers/api/v1/users_controller_spec.rb @@ -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: 'user@gmail.com', 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: 'user@gmail.com', 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: ' useR@gmail.com ', 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 'user@gmail.com' + 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: 'user@gmail.com' } + + 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