Skip to content

Commit

Permalink
Allow to log out sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
Taucher2003 committed Dec 13, 2023
1 parent 2297cb6 commit 5d57419
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 3 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ AllCops:
GraphQL/ExtractInputType:
Enabled: false

Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]


Metrics/AbcSize:
Enabled: false

Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ gem 'seed-fu', '~> 2.3'
gem 'sidekiq', '~> 7.1'

gem 'lograge', '~> 0.14.0'

gem 'declarative_policy', '~> 1.1'
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
debug_inspector (1.1.0)
declarative_policy (1.1.0)
diff-lcs (1.5.0)
docile (1.4.0)
drb (2.2.0)
Expand Down Expand Up @@ -318,6 +319,7 @@ DEPENDENCIES
bootsnap
database_cleaner-active_record (~> 2.1)
debug
declarative_policy (~> 1.1)
factory_bot_rails (~> 6.2)
graphql (~> 2.1)
lograge (~> 0.14.0)
Expand Down
6 changes: 5 additions & 1 deletion app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ def mutations_allowed?
false
end

%i[none invalid session].each do |t|
def invalid?
(authorization.nil? && !none?) || type == :invalid
end

%i[none session].each do |t|
define_method :"#{t}?" do
type == t
end
Expand Down
4 changes: 4 additions & 0 deletions app/graphql/mutations/base_mutation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ def self.require_one_of(arguments, context)
field :errors, [GraphQL::Types::String],
null: false,
description: 'Errors encountered during execution of the mutation.'

def current_user
context[:current_user]
end
end
end
22 changes: 22 additions & 0 deletions app/graphql/mutations/users/logout.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Mutations
module Users
class Logout < BaseMutation
description 'Logout an existing user session'

field :user_session, Types::UserSessionType, null: true, description: 'The logged out user session'

argument :user_session_id, Types::GlobalIdType[::UserSession], required: true,
description: 'ID of the session to logout'

def resolve(user_session_id:)
user_session = SagittariusSchema.object_from_id(user_session_id)

return { user_session: nil, errors: ['Invalid user session'] } if user_session.nil?

UserLogoutService.new(current_user, user_session).execute.to_mutation_response(success_key: :user_session)
end
end
end
end
1 change: 1 addition & 0 deletions app/graphql/types/mutation_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class MutationType < Types::BaseObject
include Sagittarius::Graphql::MountMutation

mount_mutation Mutations::Users::Login
mount_mutation Mutations::Users::Logout
mount_mutation Mutations::Users::Register

field :echo, GraphQL::Types::String, null: false,
Expand Down
2 changes: 2 additions & 0 deletions app/graphql/types/user_session_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Types
class UserSessionType < Types::BaseObject
description 'Represents a user session'

field :active, GraphQL::Types::Boolean, null: false,
description: 'Whether or not the session is active and can be used'
field :id, Types::GlobalIdType[::UserSession], null: false, description: 'GlobalID of the user'
field :token, String, null: true, description: 'Token belonging to the session, only present on creation'
field :user, Types::UserType, null: false, description: 'User that belongs to the session'
Expand Down
21 changes: 21 additions & 0 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

module Ability
module_function

def allowed?(user, ability, subject = :global)
policy = policy_for(user, subject)

policy.allowed?(ability)
end

def policy_for(user, subject = :global)
Cache.policies ||= {}

DeclarativePolicy.policy_for(user, subject, cache: Cache.policies)
end

class Cache < ActiveSupport::CurrentAttributes
attribute :policies
end
end
4 changes: 4 additions & 0 deletions app/policies/base_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

class BasePolicy < DeclarativePolicy::Base
end
7 changes: 7 additions & 0 deletions app/policies/user_session_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class UserSessionPolicy < BasePolicy
condition(:session_owner) { @subject.user_id == @user&.id }

rule { session_owner }.enable :logout_session
end
2 changes: 1 addition & 1 deletion app/services/service_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def to_mutation_response(success_key: :object)
if payload.is_a?(ActiveModel::Errors)
{ success_key => nil, errors: payload.full_messages }
else
{ success_key => nil, errors: payload }
{ success_key => nil, errors: Array.wrap(payload) }
end
end
end
26 changes: 26 additions & 0 deletions app/services/user_logout_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

class UserLogoutService
include Sagittarius::Loggable

def initialize(current_user, user_session)
@current_user = current_user
@user_session = user_session
end

def execute
unless Ability.allowed?(@current_user, :logout_session, @user_session)
return ServiceResponse.error(payload: "You can't log out this session")
end

@user_session.active = false

if @user_session.save
logger.info(message: 'Logged out session', session_id: @user_session.id, user_id: @user_session.user_id)
ServiceResponse.success(message: 'Logged out session', payload: @user_session)
else
logger.warn(message: 'Failed to log out session', session_id: @user_session.id, user_id: @user_session.user_id)
ServiceResponse.error(payload: 'Failed to log out session')
end
end
end
1 change: 1 addition & 0 deletions spec/graphql/types/user_session_type_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
id
user
token
active
]
end

Expand Down
31 changes: 31 additions & 0 deletions spec/policies/user_session_policy_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UserSessionPolicy do
subject { described_class.new(current_user, user_session) }

let(:current_user) { nil }
let(:user_session) { nil }

context 'when user is owner of the session' do
let(:current_user) { create(:user) }
let(:user_session) { create(:user_session, user: current_user) }

it { is_expected.to be_allowed(:logout_session) }
end

context 'when user is not owner of the session' do
let(:current_user) { create(:user) }
let(:user_session) { create(:user_session) }

it { is_expected.not_to be_allowed(:logout_session) }
end

context 'when user is nil' do
let(:current_user) { nil }
let(:user_session) { create(:user_session) }

it { is_expected.not_to be_allowed(:logout_session) }
end
end
74 changes: 74 additions & 0 deletions spec/requests/graphql/mutation/users/logout_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'usersLogout Mutation' do
include GraphqlHelpers

let(:mutation) do
<<~QUERY
mutation($input: UsersLogoutInput!) {
usersLogout(input: $input) {
errors
userSession {
id
active
}
}
}
QUERY
end

let(:user_session_id) { nil }
let(:current_user) { nil }
let(:variables) { { input: { userSessionId: user_session_id } } }

before { post_graphql mutation, variables: variables, current_user: current_user }

context 'when input is valid' do
let(:user_session) { create(:user_session) }
let(:user_session_id) { user_session.to_global_id.to_s }

context 'when logging out a session of the same user' do
let(:current_user) { user_session.user }

it 'logs out the session', :aggregate_failures do
expect(graphql_data_at(:users_logout, :user_session, :id)).to eq(user_session_id)
expect(graphql_data_at(:users_logout, :user_session, :active)).to be(false)
end
end

context 'when logging out a session of another user' do
let(:current_user) { create(:user) }

it 'does not log out the session', :aggregate_failures do
expect(graphql_data_at(:users_logout, :errors)).to include("You can't log out this session")
expect(graphql_data_at(:users_logout, :user_session)).to be_nil
end
end
end

context 'when input is invalid' do
let(:current_user) { create(:user) }

context 'when session id is invalid' do
let(:user_session_id) { 'some random string' }

it 'raises validation error' do
expect(graphql_errors).to include(
a_hash_including(
'message' => a_string_including("Could not coerce value \"#{user_session_id}\" to UserSessionID")
)
)
end
end

context 'when session id is does not exist' do
let(:user_session_id) { 'gid://Sagittarius/UserSession/0' }

it 'raises validation error' do
expect(graphql_data_at(:users_logout, :errors)).to include('Invalid user session')
end
end
end
end
40 changes: 40 additions & 0 deletions spec/services/user_logout_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UserLogoutService do
subject(:service_response) { described_class.new(current_user, user_session).execute }

context 'when current_user can log out user_session' do
let(:current_user) { create(:user) }
let(:user_session) { create(:user_session, user: current_user) }

it { is_expected.to be_success }

it 'changes the session to inactive' do
expect { service_response }.to change { user_session.reload.active }.from(true).to(false)
end
end

context 'when current_user can not log out user_session' do
let(:current_user) { create(:user) }
let(:user_session) { create(:user_session) }

it { is_expected.not_to be_success }

it 'does not change the session to inactive' do
expect { service_response }.not_to change { user_session.reload.active }
end
end

context 'when current_user is nil' do
let(:current_user) { nil }
let(:user_session) { create(:user_session) }

it { is_expected.not_to be_success }

it 'does not change the session to inactive' do
expect { service_response }.not_to change { user_session.reload.active }
end
end
end
2 changes: 1 addition & 1 deletion spec/support/helpers/graphql_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def graphql_dig_at(data, *path)
keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.graphql_field_name(segment) }

keys.reduce(data) do |acc, cur|
if acc.is_a?(Array) && key.is_a?(Integer)
if acc.is_a?(Array) && cur.is_a?(Integer)
acc[cur]
elsif acc.is_a?(Array)
acc.compact.pluck(cur)
Expand Down

0 comments on commit 5d57419

Please sign in to comment.