groovestack-auth 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/.rubocop.yml +51 -0
  4. data/Gemfile +12 -0
  5. data/Gemfile.lock +194 -0
  6. data/config/initializers/core_config.rb +41 -0
  7. data/config/initializers/devise.rb +314 -0
  8. data/config/initializers/devise_token_auth.rb +72 -0
  9. data/config/initializers/graphql_devise.rb +58 -0
  10. data/config/initializers/omniauth.rb +69 -0
  11. data/config/routes.rb +11 -0
  12. data/config.ru +12 -0
  13. data/db/migrate/20231103172517_create_users.rb +54 -0
  14. data/db/migrate/20231103174037_create_identities.rb +19 -0
  15. data/docs/apple-oauth-local-dev.adoc +67 -0
  16. data/groovestack-auth.gemspec +41 -0
  17. data/lib/fabricators/user_fabricator.rb +17 -0
  18. data/lib/graphql/identity/filter.rb +13 -0
  19. data/lib/graphql/identity/mutations.rb +27 -0
  20. data/lib/graphql/identity/queries.rb +25 -0
  21. data/lib/graphql/identity/type.rb +22 -0
  22. data/lib/graphql/user/filter.rb +15 -0
  23. data/lib/graphql/user/mutations.rb +63 -0
  24. data/lib/graphql/user/queries.rb +40 -0
  25. data/lib/graphql/user/type.rb +30 -0
  26. data/lib/groovestack/auth/action_cable.rb +29 -0
  27. data/lib/groovestack/auth/authenticated_api_controller.rb +13 -0
  28. data/lib/groovestack/auth/omniauth_callbacks_controller.rb +111 -0
  29. data/lib/groovestack/auth/provider.rb +59 -0
  30. data/lib/groovestack/auth/providers/apple.rb +26 -0
  31. data/lib/groovestack/auth/providers/email.rb +15 -0
  32. data/lib/groovestack/auth/providers/google.rb +17 -0
  33. data/lib/groovestack/auth/providers/omni_auth.rb +47 -0
  34. data/lib/groovestack/auth/railtie.rb +64 -0
  35. data/lib/groovestack/auth/schema_plugin.rb +19 -0
  36. data/lib/groovestack/auth/version.rb +7 -0
  37. data/lib/groovestack/auth.rb +108 -0
  38. data/lib/identity.rb +31 -0
  39. data/lib/user.rb +53 -0
  40. data/lib/users/roles.rb +42 -0
  41. data/readme.adoc +52 -0
  42. metadata +144 -0
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(DeviseTokenAuth)
4
+ DeviseTokenAuth.setup do |config|
5
+ # By default the authorization headers will change after each request. The
6
+ # client is responsible for keeping track of the changing tokens. Change
7
+ # this to false to prevent the Authorization header from changing after
8
+ # each request.
9
+ config.change_headers_on_each_request = false
10
+
11
+ # By default, users will need to re-authenticate after 2 weeks. This setting
12
+ # determines how long tokens will remain valid after they are issued.
13
+ # config.token_lifespan = 2.weeks
14
+
15
+ # Limiting the token_cost to just 4 in testing will increase the performance of
16
+ # your test suite dramatically. The possible cost value is within range from 4
17
+ # to 31. It is recommended to not use a value more than 10 in other environments.
18
+ config.token_cost = Rails.env.test? ? 4 : 10
19
+
20
+ # Sets the max number of concurrent devices per user, which is 10 by default.
21
+ # After this limit is reached, the oldest tokens will be removed.
22
+ # config.max_number_of_devices = 10
23
+
24
+ # Sometimes it's necessary to make several requests to the API at the same
25
+ # time. In this case, each request in the batch will need to share the same
26
+ # auth token. This setting determines how far apart the requests can be while
27
+ # still using the same auth token.
28
+ # config.batch_request_buffer_throttle = 5.seconds
29
+
30
+ # This route will be the prefix for all oauth2 redirect callbacks. For
31
+ # example, using the default '/omniauth', the github oauth2 provider will
32
+ # redirect successful authentications to '/omniauth/github/callback'
33
+ config.omniauth_prefix = '/users/auth'
34
+
35
+ config.cookie_attributes = {
36
+ expires: 10.seconds
37
+ }
38
+
39
+ # By default sending current password is not needed for the password update.
40
+ # Uncomment to enforce current_password param to be checked before all
41
+ # attribute updates. Set it to :password if you want it to be checked only if
42
+ # password is updated.
43
+ # config.check_current_password_before_update = :attributes
44
+
45
+ # By default we will use callbacks for single omniauth.
46
+ # It depends on fields like email, provider and uid.
47
+ config.default_callbacks = false
48
+
49
+ # Makes it possible to change the headers names
50
+ config.headers_names = {
51
+ authorization: 'Authorization',
52
+ 'access-token': 'access-token',
53
+ client: 'client',
54
+ expiry: 'expiry',
55
+ id: 'id',
56
+ 'token-type': 'token-type'
57
+ }
58
+
59
+ # Makes it possible to use custom uid column
60
+ config.other_uid = 'id'
61
+
62
+ # By default, only Bearer Token authentication is implemented out of the box.
63
+ # If, however, you wish to integrate with legacy Devise authentication, you can
64
+ # do so by enabling this flag. NOTE: This feature is highly experimental!
65
+ # config.enable_standard_devise_support = false
66
+
67
+ # By default DeviseTokenAuth will not send confirmation email, even when including
68
+ # devise confirmable module. If you want to use devise confirmable module and
69
+ # send email, set it to true. (This is a setting for compatibility)
70
+ # config.send_confirmation_email = true
71
+ end
72
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(GraphqlDevise)
4
+ ActiveSupport.on_load(:after_initialize) do # rubocop:disable Metrics/BlockLength
5
+ module DeviseTokenAuth
6
+ module Concerns
7
+ module ActiveRecordSupport
8
+ extend ActiveSupport::Concern
9
+
10
+ class_methods do
11
+ # Override to remove provider from attrs b/c use identity model
12
+ def dta_find_by(attrs = {})
13
+ attrs.delete(:provider)
14
+ attrs.delete('provider')
15
+ find_by(attrs)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ class User < ActiveRecord::Base
23
+ # Override to remove provider from attrs b/c use identity model
24
+ def self.dta_find_by(attrs = {})
25
+ attrs.delete(:provider)
26
+ attrs.delete('provider')
27
+ find_by(attrs)
28
+ end
29
+ end
30
+
31
+ User.include ::GraphqlDevise::Authenticatable
32
+
33
+ module AugmentedGraphqlDeviseRegisterArgs
34
+ extend ActiveSupport::Concern
35
+
36
+ included do
37
+ argument :name, String, required: true
38
+ end
39
+ end
40
+
41
+ GraphqlDevise::Mutations::Register.include AugmentedGraphqlDeviseRegisterArgs
42
+
43
+ GraphqlDevise::Mutations::Register.class_eval do
44
+ private
45
+
46
+ def build_resource(attrs)
47
+ # NOTE: remove provider from attrs b/c use identity model
48
+ attrs.delete(:provider)
49
+ attrs[:roles] = [Users::Roles::Role::ADMIN] unless Core::Config::App.generate_config[:has_admins]
50
+ resource_class.new(attrs)
51
+ end
52
+ end
53
+
54
+ GraphqlDevise::Types::AuthenticatableType.class_eval do
55
+ field :id, GraphQL::Types::ID, null: false
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.config.middleware.use OmniAuth::Builder do
4
+ Groovestack::Auth.configured_providers(ancestor: Groovestack::Auth::Providers::OmniAuth).each do |p|
5
+ provider(*p.generate_omniauth_args)
6
+ end
7
+ end
8
+
9
+ module Groovestack
10
+ module Auth
11
+ class OmniauthFailureEndpoint < OmniAuth::FailureEndpoint
12
+ def call
13
+ # raise_out! if OmniAuth.config.failure_raise_out_environments.include?(ENV['RACK_ENV'].to_s)
14
+ redirect_to_failure
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ OmniAuth.config.on_failure = Groovestack::Auth::OmniauthFailureEndpoint
21
+
22
+ module OmniAuth
23
+ module Strategies
24
+ class Apple < OmniAuth::Strategies::OAuth2
25
+ def request_phase
26
+ # https://github.com/omniauth/omniauth/issues/975
27
+
28
+ # since apple uses POST to callback, can't store in session
29
+ # need to store omniauth params as a cookie
30
+
31
+ auth_params = authorize_params # add state & nonce to session values to persisted cookies
32
+
33
+ cookies.encrypted['apple_omniauth_params'] = {
34
+ same_site: :none,
35
+ expires: 1.minute.from_now,
36
+ secure: true,
37
+ value: JSON.generate({ origin: session['omniauth.origin'] || request.env['HTTP_REFERER'],
38
+ state: auth_params[:state], nonce: auth_params[:nonce] })
39
+ }
40
+
41
+ super
42
+ end
43
+
44
+ def callback_phase # rubocop:disable Metrics/AbcSize
45
+ # add omniauth params back to session
46
+
47
+ apple_omniauth_params = JSON.parse(cookies.encrypted['apple_omniauth_params'])
48
+ cookies.delete('apple_omniauth_params')
49
+ env['omniauth.origin'] = apple_omniauth_params['origin']
50
+ session['omniauth.origin'] = apple_omniauth_params['origin']
51
+ session['omniauth.state'] = apple_omniauth_params['state']
52
+ session['omniauth.nonce'] = apple_omniauth_params['nonce']
53
+
54
+ super
55
+ end
56
+
57
+ def authorize_params
58
+ # memoize so they aren't regenerated in the request_phase super call
59
+ @authorize_params ||= super.merge(nonce: new_nonce)
60
+ end
61
+
62
+ private
63
+
64
+ def cookies
65
+ request.env['action_dispatch.cookies']
66
+ end
67
+ end
68
+ end
69
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Groovestack::Auth::Railtie.routes.draw do
4
+ if defined?(Devise)
5
+ devise_scope :user do
6
+ match '/users/auth/:provider/callback', to: 'groovestack/auth/omniauth_callbacks#verified', via: %i[get post],
7
+ as: :omniauth_callback
8
+ get '/users/auth/failure', to: 'groovestack/auth/omniauth_callbacks#omniauth_failure', as: :omniauth_failure
9
+ end
10
+ end
11
+ end
data/config.ru ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.schema_format = :sql
9
+ Combustion.initialize! :active_record
10
+
11
+ # Combustion.initialize! :all
12
+ run Combustion::Application
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dynamic rails major version as recommended by perplexity
4
+ class CreateUsers < ActiveRecord::Migration[Gem::Version.new(Rails.version).segments.first.to_f]
5
+ def change # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
6
+ create_table :users, id: :uuid do |t|
7
+ ## Database authenticatable
8
+ t.string :encrypted_password, null: false, default: ''
9
+
10
+ ## Recoverable
11
+ t.string :reset_password_token
12
+ t.datetime :reset_password_sent_at
13
+ t.boolean :allow_password_change, default: false, null: false
14
+
15
+ ## Rememberable
16
+ t.datetime :remember_created_at
17
+
18
+ ## Trackable
19
+ t.integer :sign_in_count, default: 0, null: false
20
+ t.datetime :current_sign_in_at
21
+ t.datetime :last_sign_in_at
22
+ t.inet :current_sign_in_ip
23
+ t.inet :last_sign_in_ip
24
+
25
+ ## Confirmable
26
+ t.string :confirmation_token
27
+ t.datetime :confirmed_at
28
+ t.datetime :confirmation_sent_at
29
+ t.string :unconfirmed_email # Only if using reconfirmable
30
+
31
+ # Lockable
32
+ # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts
33
+ # t.string :unlock_token # Only if unlock strategy is :email or :both
34
+ # t.datetime :locked_at
35
+
36
+ ## User Info
37
+ t.string :name
38
+ t.string :email
39
+ t.jsonb :roles, null: false, default: []
40
+ t.string :language
41
+ t.string :image
42
+
43
+ ## Tokens
44
+ t.json :tokens
45
+
46
+ t.timestamps
47
+ end
48
+
49
+ add_index :users, :email, unique: true
50
+ add_index :users, :reset_password_token, unique: true
51
+ add_index :users, :confirmation_token, unique: true
52
+ # add_index :users, :unlock_token, unique: true
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # dynamic rails major version as recommended by perplexity
4
+ class CreateIdentities < ActiveRecord::Migration[Gem::Version.new(Rails.version).segments.first.to_f]
5
+ def change
6
+ create_table :identities, id: :uuid do |t|
7
+ t.string :provider
8
+ t.string :uid
9
+ t.jsonb :omniauth_data
10
+
11
+ t.references :user, foreign_key: true, index: false, null: false, type: :uuid
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :identities, %i[user_id provider], unique: true
17
+ add_index :identities, %i[provider uid], unique: true
18
+ end
19
+ end
@@ -0,0 +1,67 @@
1
+ = Local Development with Apple OAuth & create-groovestack
2
+
3
+ Sign-In with Apple does not allow `localhost` as a valid callback URL. It also requires SSL. This means that you won't be able to test your Apple OAuth integration out of the box. To enable local development with Apple OAuth in your bootstrapped Groovestack app, you have a few options.
4
+
5
+ == ngrok (or similar)
6
+ Use a tool like https://ngrok.com/[ngrok] to create a temporary public https:// URL for your local development environment. Add this URL to your Apple OAuth config as well as the callback url ending with `/users/auth/apple/callback`. For example, if your ngrok URL is `https://12345678.ngrok.io`, your Apple OAuth domain would be `12345678.ngrok.io` and your callback URL would be `https://12345678.ngrok.io/users/auth/apple/callback.`
7
+
8
+ == Custom Host Entry
9
+ Add a custom host entry to your `/etc/hosts` file for a testing URL and point it to your local machine. For example, you could add the following to your `/etc/hosts` file:
10
+
11
+ [source,shell]
12
+ ----
13
+ 127.0.0.1 local.groovestack-demo.com
14
+ ----
15
+
16
+ Register the custom host with your Apple OAuth config. I.e. set the domain to `local.groovestack-demo.com` and the callback URL to `https://local.groovestack-demo.com:3000/users/auth/apple/callback`. (create-groovestack runs on port 3000 out of the box so be sure to include this in your callback URL).
17
+
18
+ Now generate a self-signed certificate for `local.groovestack-demo.com` using https://github.com/FiloSottile/mkcert[mkcert].
19
+
20
+ [source,shell]
21
+ ----
22
+ mkcert local.groovestack-demo.com
23
+ ----
24
+ This will generate a `local.groovestack-demo.com.pem` and `local.groovestack-demo.com-key.pem` file in your current directory. Make sure the new cert is marked as trusted.
25
+
26
+ Finally, enable SSL in your rails vite app.
27
+
28
+ === Procfile.dev
29
+
30
+ Update your Procfile.dev web command to serve ssl
31
+ [source,ruby]
32
+ ----
33
+ web: bin/rails s -b 'ssl://localhost:3000?key=local.groovestack-demo.com-key.pem&cert=local.groovestack-demo.com.pem&verify_mode=none'
34
+ ----
35
+
36
+ === Vite
37
+
38
+ Add the following development options in your `vite.json` file.
39
+ [source, ts]
40
+ ----
41
+ "host": "local.groovestack-demo.com",
42
+ "https": true,
43
+ ----
44
+
45
+ Add the following server options to your `vite.config.ts` file.
46
+ [source, ts]
47
+ ----
48
+ import { existsSync, readFileSync } from "fs";
49
+ import { resolve } from "path";
50
+
51
+ // resolve your certificate
52
+ const certPath = resolve('.', "local.groovestack-demo.com.pem");
53
+ const keyPath = resolve('.', "local.groovestack-demo.com-key.pem");
54
+ const https = existsSync(certPath) ? { key: readFileSync(keyPath), cert: readFileSync(certPath) } : {};
55
+
56
+ // add the server config options
57
+ server: {
58
+ host: 'local.groovestack-demo.com',
59
+ https
60
+ },
61
+ ----
62
+
63
+ Finally, don't forget to add your new host to your `development.rb` file.
64
+ [source,ruby]
65
+ ----
66
+ config.hosts << "local.groovestack-demo.com"
67
+ ----
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/groovestack/auth/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'groovestack-auth'
7
+ spec.version = Groovestack::Auth::VERSION
8
+ spec.authors = ['Max Schridde']
9
+ spec.email = ['maxjschridde@gmail.com']
10
+
11
+ spec.summary = 'Groovestack extension for application authentication'
12
+ spec.description = 'Groovestack::Auth is an authentication extension for the Groovestack Platform.'
13
+ spec.post_install_message = 'Groovestack::Auth installed'
14
+
15
+ spec.homepage = 'https://github.com/talysto/groovestack-core/'
16
+ spec.required_ruby_version = '>= 3.1.0'
17
+
18
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/talysto/groovestack-core/'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/talysto/groovestack-core/'
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_dependency 'groovestack-base', '>= 0.1.7'
34
+ spec.add_dependency 'jsonb_accessor', '~>1.4'
35
+
36
+ # spec.add_development_dependency 'graphql_devise'
37
+ spec.add_dependency 'omniauth-apple'
38
+ spec.add_dependency 'omniauth-google-oauth2'
39
+
40
+ spec.metadata['rubygems_mfa_required'] = 'true'
41
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fabrication'
4
+ require 'faker'
5
+
6
+ Fabricator(:user) do
7
+ name { Faker::Name.name }
8
+ password { Devise.friendly_token[0, 20] }
9
+ end
10
+
11
+ Fabricator(:user_with_email, from: :user) do
12
+ email { Faker::Internet.email }
13
+ end
14
+
15
+ Fabricator(:admin_user, from: :user_with_email) do
16
+ roles [User::Role::ADMIN]
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Identity
5
+ class Filter < ::Groovestack::Base::GraphQL::BaseInputObject
6
+ description 'Identity filter props'
7
+
8
+ argument :ids, [ID], required: false
9
+
10
+ argument :user_id, ID, required: false
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Identity
5
+ module Mutations
6
+ class Delete < ::Groovestack::Base::GraphQL::BaseMutation
7
+ argument :id, ID, required: true
8
+
9
+ type ::GraphQL::Identity::Type
10
+
11
+ def perform(id:)
12
+ identity = ::Identity.find(id)
13
+
14
+ identity.destroy!
15
+
16
+ identity
17
+ end
18
+ end
19
+
20
+ extend ActiveSupport::Concern
21
+
22
+ included do
23
+ field :delete_identity, mutation: ::GraphQL::Identity::Mutations::Delete, description: 'delete identity'
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Identity
5
+ module Queries
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ::Groovestack::Base::GraphQL::Providers::ReactAdmin::Resource
10
+
11
+ react_admin_resource :identities, graphql_path: 'GraphQL'
12
+ end
13
+
14
+ def identities_scope(base_scope:, sort_field: nil, sort_order: nil, filter: {})
15
+ scope = base_scope
16
+ scope = scope.where(id: filter.ids) if filter.ids.present?
17
+ scope = scope.where(user_id: filter.user_id) if filter.user_id.present?
18
+
19
+ return scope if sort_field.blank?
20
+
21
+ scope.order({ sort_field.underscore => sort_order || 'desc' })
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Identity
5
+ class Type < ::Groovestack::Base::GraphQL::Types::BaseObject
6
+ description 'An identity'
7
+
8
+ graphql_name 'Identity'
9
+
10
+ field :created_at, ::GraphQL::Types::ISO8601DateTime, null: false, description: 'created at'
11
+ field :id, ID, null: false, description: 'id'
12
+ field :uid, String, null: false, description: 'uid'
13
+ field :updated_at, ::GraphQL::Types::ISO8601DateTime, null: false, description: 'updated at'
14
+
15
+ field :provider, String, null: true, description: 'provider'
16
+
17
+ # associations
18
+
19
+ field :user_id, ID, null: false, description: 'user id of the user the identity is associated with'
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module User
5
+ class Filter < ::Groovestack::Base::GraphQL::BaseInputObject
6
+ description 'User filter props'
7
+
8
+ argument :ids, [ID], required: false
9
+ argument :q, String, required: false
10
+
11
+ argument :name, String, required: false
12
+ argument :roles, [String], required: false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module User
5
+ module Mutations
6
+ class Update < ::Groovestack::Base::GraphQL::BaseMutation
7
+ argument :current_password, String, required: false
8
+ argument :email, String, required: false
9
+ argument :id, ID, required: true
10
+ argument :image, String, required: false
11
+ argument :language, String, required: false
12
+ argument :name, String, required: false
13
+ argument :password, String, required: false
14
+ argument :roles, [String], required: false
15
+
16
+ type ::GraphQL::User::Type
17
+
18
+ def current_user
19
+ context[:current_resource]
20
+ end
21
+
22
+ def perform(id:, **attrs)
23
+ obj = id == current_user&.id ? current_user : ::User.find(id)
24
+
25
+ return update_with_password!(obj, **attrs) if attrs.keys.intersect?(%i[password current_password])
26
+
27
+ if !current_user.admin? && attrs[:roles].present?
28
+ raise GraphQL::ExecutionError,
29
+ 'Validation Failed: only admins can update user roles'
30
+ end
31
+
32
+ obj.update!(attrs)
33
+
34
+ obj
35
+ end
36
+
37
+ def update_with_password!(user, **attrs) # rubocop:disable Metrics/AbcSize
38
+ unless current_user&.id == user.id
39
+ raise GraphQL::ExecutionError,
40
+ 'Validation Failed: user can only update their own password'
41
+ end
42
+
43
+ ::User.transaction do
44
+ # if password isn't set yet, allow them to set it
45
+ user.password = attrs[:current_password] = attrs[:password] unless user.email_provider?
46
+
47
+ raise GraphQL::ExecutionError, user.errors.full_messages.join(' ') unless user.update_with_password(attrs)
48
+
49
+ context[:bypass_sign_in].call user.reload, scope: :user
50
+ end
51
+
52
+ user
53
+ end
54
+ end
55
+
56
+ extend ActiveSupport::Concern
57
+
58
+ included do
59
+ field :update_user, mutation: ::GraphQL::User::Mutations::Update, description: 'update user'
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module User
5
+ module Queries
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ::Groovestack::Base::GraphQL::Providers::ReactAdmin::Resource
10
+
11
+ react_admin_resource :users, graphql_path: 'GraphQL'
12
+
13
+ def User(id:) # rubocop:disable Naming/MethodName
14
+ id == 'me' ? current_user : ::User.find(id)
15
+ end
16
+ end
17
+
18
+ def current_user
19
+ context[:current_resource]
20
+ end
21
+
22
+ def users_scope(base_scope:, sort_field: nil, sort_order: nil, filter: {}) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
23
+ scope = base_scope
24
+ scope = scope.where(id: filter.ids) if filter.ids.present?
25
+
26
+ if filter.q.present?
27
+ scope = scope.where(id: scope.fuzzysearch(filter.q)).or(scope.where(id: scope.emailsearch(filter.q)))
28
+ end
29
+ scope = scope.fuzzysearch(filter.name) if filter.name.present?
30
+ scope = scope.with_roles(filter.roles) if filter.roles.present?
31
+
32
+ return scope if sort_field.blank?
33
+
34
+ sort_field = 'last_sign_in_at' if sort_field == 'last_login_at'
35
+
36
+ scope.order({ sort_field.underscore => sort_order || 'desc' })
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module User
5
+ class Type < ::Groovestack::Base::GraphQL::Types::BaseObject
6
+ include ::Groovestack::Base::GraphQL::Helpers::Types::Typified
7
+
8
+ description 'An user'
9
+
10
+ field :created_at, ::GraphQL::Types::ISO8601DateTime, null: false, description: 'created at'
11
+ field :id, ID, null: false, description: 'id'
12
+ field :updated_at, ::GraphQL::Types::ISO8601DateTime, null: false, description: 'updated at'
13
+
14
+ field :email, String, null: true, description: 'email'
15
+ field :has_email_provider, Boolean, null: true, description: 'user has email provider',
16
+ method: :email_provider?
17
+ field :image, String, null: true, description: 'user image url'
18
+ field :language, String, null: true, description: 'user language'
19
+ field :name, String, null: true, description: 'name'
20
+ field :roles, [String], null: true, description: 'roles'
21
+
22
+ # devise fields
23
+ field :last_login_at, ::GraphQL::Types::ISO8601DateTime, null: true, description: 'last login in at',
24
+ method: :last_sign_in_at
25
+ field :sign_in_count, Integer, null: true, description: 'sign in count'
26
+
27
+ field :identities, [::GraphQL::Identity::Type], null: false, description: 'identities'
28
+ end
29
+ end
30
+ end