Skip to content
Open
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
33 changes: 33 additions & 0 deletions app/graphql/mutations/users/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module Mutations
module Users
class Create < BaseMutation
description 'Admin-create a user.'

field :user, Types::UserType, null: true, description: 'The created user.'

argument :admin, Boolean, required: false, description: 'Admin status for the user.'
argument :email, String, required: true, description: 'Email for the user.'
argument :firstname, String, required: false, description: 'Firstname for the user.'
argument :lastname, String, required: false, description: 'Lastname for the user.'
argument :password, String, required: true, description: 'Password for the user.'
argument :password_repeat,
String,
required: true,
description: 'Password repeat for the user to check for typos.'
argument :username, String, required: true, description: 'Username for the user.'

def resolve(**params)
if params[:password] != params.delete(:password_repeat)
return { user: nil, errors: [create_error(:invalid_password_repeat, 'Invalid password repeat')] }
end

::Users::CreateService.new(
current_authentication,
**params
).execute.to_mutation_response(success_key: :user)
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 @@ -35,6 +35,7 @@ class MutationType < Types::BaseObject
mount_mutation Mutations::Users::Mfa::BackupCodes::Rotate
mount_mutation Mutations::Users::Mfa::Totp::GenerateSecret
mount_mutation Mutations::Users::Mfa::Totp::ValidateSecret
mount_mutation Mutations::Users::Create
mount_mutation Mutations::Users::EmailVerification
mount_mutation Mutations::Users::Login
mount_mutation Mutations::Users::Logout
Expand Down
1 change: 1 addition & 0 deletions app/models/audit_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class AuditEvent < ApplicationRecord
email_verified: 35,
password_reset_requested: 36,
password_reset: 37,
user_created: 39,
}.with_indifferent_access

# rubocop:disable Lint/StructNewOverride
Expand Down
1 change: 1 addition & 0 deletions app/policies/global_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ class GlobalPolicy < BasePolicy
enable :delete_runtime
enable :rotate_runtime_token
enable :list_users
enable :create_user
end
end
38 changes: 38 additions & 0 deletions app/services/users/create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Users
class CreateService
include Sagittarius::Database::Transactional

attr_reader :current_authentication, :params

def initialize(current_authentication, **params)
@current_authentication = current_authentication
@params = params
end

def execute
unless Ability.allowed?(current_authentication, :create_user, :global)
return ServiceResponse.error(message: 'Missing permissions', error_code: :missing_permission)
end

transactional do
user = User.create(**params)
unless user.persisted?
return ServiceResponse.error(message: 'User is invalid', error_code: :invalid_user,
details: user.errors)
end

AuditService.audit(
:user_created,
author_id: current_authentication.user.id,
entity: user,
details: { **params.except(:password) },
target: AuditEvent::GLOBAL_TARGET
)

ServiceResponse.success(payload: user)
end
end
end
end
26 changes: 26 additions & 0 deletions docs/graphql/mutation/userscreate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: usersCreate
---

Admin-create a user.

## Arguments

| Name | Type | Description |
|------|------|-------------|
| `admin` | [`Boolean`](../scalar/boolean.md) | Admin status for the user. |
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
| `email` | [`String!`](../scalar/string.md) | Email for the user. |
| `firstname` | [`String`](../scalar/string.md) | Firstname for the user. |
| `lastname` | [`String`](../scalar/string.md) | Lastname for the user. |
| `password` | [`String!`](../scalar/string.md) | Password for the user. |
| `passwordRepeat` | [`String!`](../scalar/string.md) | Password repeat for the user to check for typos. |
| `username` | [`String!`](../scalar/string.md) | Username for the user. |

## Fields

| Name | Type | Description |
|------|------|-------------|
| `clientMutationId` | [`String`](../scalar/string.md) | A unique identifier for the client performing the mutation. |
| `errors` | [`[Error!]!`](../object/error.md) | Errors encountered during execution of the mutation. |
| `user` | [`User`](../object/user.md) | The created user. |
113 changes: 113 additions & 0 deletions spec/requests/graphql/mutation/users/create_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'usersCreate Mutation' do
include GraphqlHelpers

let(:mutation) do
<<~QUERY
mutation($input: UsersCreateInput!) {
usersCreate(input: $input) {
#{error_query}
user {
id
email
username
firstname
lastname
admin
}
}
}
QUERY
end

let(:variables) { { input: input } }
let(:password) { 'Password123!' }

before do
post_graphql mutation, variables: variables, current_user: current_user
end

context 'when creating a user as admin' do
let(:current_user) { create(:user, :admin) }
let(:input) do
{
email: generate(:email),
username: generate(:username),
password: password,
passwordRepeat: password,
firstname: 'Graph',
lastname: 'QL',
admin: false,
}
end

it 'creates user' do
expect(graphql_data_at(:users_create, :user, :id)).to be_present
expect(graphql_data_at(:users_create, :user, :email)).to eq(input[:email])
expect(graphql_data_at(:users_create, :user, :username)).to eq(input[:username])

user = SagittariusSchema.object_from_id(graphql_data_at(:users_create, :user, :id))

is_expected.to create_audit_event(
:user_created,
author_id: current_user.id,
entity_id: user.id,
entity_type: 'User',
details: {
'email' => input[:email],
'username' => input[:username],
'firstname' => input[:firstname],
'lastname' => input[:lastname],
'admin' => input[:admin],
},
target_id: 0,
target_type: 'global'
)
end
end

context 'when non-admin attempts to create a user' do
let(:current_user) { create(:user) }
let(:input) do
{
email: generate(:email),
username: generate(:username),
password: password,
passwordRepeat: password,
firstname: 'Graph',
lastname: 'QL',
admin: false,
}
end

it 'does not create user and returns an error' do
expect(graphql_data_at(:users_create, :user)).to be_nil
expect(graphql_data_at(:users_create, :errors)).to be_present
is_expected.not_to create_audit_event
end
end

context 'when password repeat does not match' do
let(:current_user) { create(:user, :admin) }
let(:input) do
{
email: generate(:email),
username: generate(:username),
password: password,
passwordRepeat: 'mismatch',
firstname: 'Graph',
lastname: 'QL',
admin: false,
}
end

it 'returns a validation error and does not create the user' do
expect(graphql_data_at(:users_create, :user)).to be_nil
expect(graphql_data_at(:users_create, :errors)).to be_present
is_expected.not_to create_audit_event
end
end
end
101 changes: 101 additions & 0 deletions spec/services/users/create_service_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Users::CreateService do
subject(:service_response) do
described_class.new(create_authentication(current_user), **params).execute
end

let!(:current_user) { create(:user, :admin) }
let(:params) do
{
email: generate(:email),
username: generate(:username),
password: 'Password123!',
firstname: 'Test',
lastname: 'User',
admin: false,
}
end

shared_examples 'does not create' do
it { is_expected.to be_error }

it 'does not create user' do
expect { service_response }.not_to change { User.count }
end

it { expect { service_response }.not_to create_audit_event }
end

context 'when not authenticated' do
let(:current_user) { nil }

it_behaves_like 'does not create'
end

context 'when params are invalid' do
context 'when email is invalid' do
let(:params) do
{
email: 'invalid-email',
username: generate(:username),
password: 'pw',
firstname: 'T',
lastname: 'U',
admin: false,
}
end

it_behaves_like 'does not create'
end

context 'when username is too long' do
let(:params) do
{
email: generate(:email),
username: 'a' * 100,
password: 'Password123!',
firstname: 'T',
lastname: 'U',
admin: false,
}
end

it_behaves_like 'does not create'
end
end

context 'when user and params are valid and user is admin' do
let(:params) do
{
email: generate(:email),
username: generate(:username),
password: 'Password123!',
firstname: 'First',
lastname: 'Last',
admin: true,
}
end

it { is_expected.to be_success }
it { expect(service_response.payload.reload).to be_valid }

it do
is_expected.to create_audit_event(
:user_created,
author_id: current_user.id,
entity_type: 'User',
details: {
'email' => params[:email],
'username' => params[:username],
'firstname' => params[:firstname],
'lastname' => params[:lastname],
'admin' => params[:admin],
},
target_type: 'global'
)
end
end
end