groovestack-auth 0.1.5 → 0.1.6

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +8 -1
  3. data/Gemfile.lock +120 -20
  4. data/{lib/groovestack/auth/action_cable.rb → app/channels/groovestack/auth/action_cable/connection.rb} +2 -2
  5. data/app/controllers/concerns/groovestack/auth/graphql/controllers/auth_helpers.rb +69 -0
  6. data/app/controllers/concerns/groovestack/auth/graphql/controllers/authed_execute.rb +16 -0
  7. data/app/controllers/groovestack/auth/authenticated_api_controller.rb +10 -0
  8. data/app/controllers/groovestack/auth/omniauth_callbacks_controller.rb +138 -0
  9. data/app/controllers/groovestack/auth/passwordless/magic_links_controller.rb +58 -0
  10. data/app/controllers/groovestack/auth/passwordless/sessions_controller.rb +75 -0
  11. data/app/graphql/graphql/identity_extensions.rb +11 -0
  12. data/app/graphql/graphql/user_extensions.rb +14 -0
  13. data/app/models/concerns/groovestack/auth/authorized_fields_for_serialization.rb +21 -0
  14. data/app/models/concerns/groovestack/auth/identity.rb +39 -0
  15. data/app/models/concerns/groovestack/auth/user.rb +14 -0
  16. data/app/views/devise/mailer/magic_link.html.erb +9 -0
  17. data/config/initializers/core_config.rb +0 -6
  18. data/config/initializers/devise.rb +387 -302
  19. data/config/initializers/omniauth.rb +0 -19
  20. data/config/locales/devise.en.yml +71 -0
  21. data/db/migrate/20231103174050_add_devise_to_users_and_identities.rb +59 -0
  22. data/groovestack-auth.gemspec +7 -7
  23. data/lib/groovestack/auth/{railtie.rb → engine.rb} +13 -2
  24. data/lib/groovestack/auth/graphql/authorized_field.rb +19 -0
  25. data/lib/groovestack/auth/graphql/authorized_object.rb +11 -0
  26. data/lib/groovestack/auth/graphql/schema_visibility.rb +40 -0
  27. data/lib/groovestack/auth/graphql/visible_field.rb +21 -0
  28. data/lib/groovestack/auth/graphql/visible_object.rb +17 -0
  29. data/lib/groovestack/auth/passwordless/t_otp_tokenizer.rb +89 -0
  30. data/lib/groovestack/auth/provider.rb +7 -0
  31. data/lib/groovestack/auth/providers/apple.rb +5 -5
  32. data/lib/groovestack/auth/providers/facebook.rb +17 -0
  33. data/lib/groovestack/auth/providers/google.rb +1 -1
  34. data/lib/groovestack/auth/providers/omni_auth.rb +2 -2
  35. data/lib/groovestack/auth/routes.rb +26 -0
  36. data/lib/groovestack/auth/settings.rb +43 -0
  37. data/lib/groovestack/auth/version.rb +1 -1
  38. data/lib/groovestack/auth.rb +33 -83
  39. metadata +55 -50
  40. data/config/initializers/devise_token_auth.rb +0 -72
  41. data/config/initializers/graphql_devise.rb +0 -58
  42. data/config/routes.rb +0 -11
  43. data/db/migrate/20231103172517_create_users.rb +0 -54
  44. data/db/migrate/20231103174037_create_identities.rb +0 -19
  45. data/lib/fabricators/user_fabricator.rb +0 -17
  46. data/lib/graphql/identity/filter.rb +0 -13
  47. data/lib/graphql/identity/mutations.rb +0 -27
  48. data/lib/graphql/identity/queries.rb +0 -25
  49. data/lib/graphql/identity/type.rb +0 -22
  50. data/lib/graphql/user/filter.rb +0 -15
  51. data/lib/graphql/user/mutations.rb +0 -63
  52. data/lib/graphql/user/queries.rb +0 -40
  53. data/lib/graphql/user/type.rb +0 -30
  54. data/lib/groovestack/auth/authenticated_api_controller.rb +0 -13
  55. data/lib/groovestack/auth/omniauth_callbacks_controller.rb +0 -111
  56. data/lib/groovestack/auth/schema_plugin.rb +0 -19
  57. data/lib/identity.rb +0 -31
  58. data/lib/user.rb +0 -53
  59. data/lib/users/roles.rb +0 -42
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module User
5
- module Mutations
6
- class Update < ::Groovestack::Base::GraphQL::Base::Mutation
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
@@ -1,40 +0,0 @@
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
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module User
5
- class Type < ::Groovestack::Base::GraphQL::Base::Object
6
- include ::Groovestack::Base::GraphQL::Types::TypeResolver
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
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'devise_token_auth/concerns/resource_finder'
4
- require 'devise_token_auth/concerns/set_user_by_token'
5
-
6
- module Groovestack
7
- module Auth
8
- class AuthenticatedApiController < ApplicationController
9
- include GraphqlDevise::SetUserByToken
10
- include Devise::Controllers::Helpers
11
- end
12
- end
13
- end
@@ -1,111 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'devise_helper'
4
- require 'devise_controller'
5
- require 'devise_token_auth/concerns/resource_finder'
6
- require 'devise_token_auth/concerns/set_user_by_token'
7
- require 'devise_token_auth/application_controller'
8
- require 'devise_token_auth/omniauth_callbacks_controller'
9
-
10
- module Groovestack
11
- module Auth
12
- class OmniauthCallbacksController < DeviseTokenAuth::OmniauthCallbacksController
13
- def verified_request?
14
- # required b/c apple uses POST to callback and resets the origin header
15
- # source: https://github.com/rails/rails/blob/6b93fff8af32ef5e91f4ec3cfffb081d0553faf0/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L442C11-L442C27
16
-
17
- apple_success_callback_request? || super
18
- end
19
-
20
- def apple_success_callback_request?
21
- params[:action] == 'verified' && params[:provider] == 'apple' && request.headers['Origin'] == 'https://appleid.apple.com'
22
- end
23
-
24
- def auth_hash
25
- @auth_hash ||= request.env['omniauth.auth'] # goes through plain old omniauth so need to override
26
- end
27
-
28
- def resource_class(_mapping = nil)
29
- # TODO: this is required b/c their code relies on the resource_class
30
- # method which expects access to the 'dta.omniauth.params' which we
31
- # don't currently have access through b/c we are going directly
32
- # through omniauth
33
-
34
- return @resource_class if defined?(@resource_class)
35
-
36
- raise 'No resource_class found' if @resource.blank?
37
-
38
- @resource_class = @resource.class
39
-
40
- @resource_class
41
- end
42
-
43
- def create_auth_params
44
- @auth_params = {
45
- auth_token: @token.token,
46
- client_id: @token.client,
47
- # uid: @resource.uid,
48
- expiry: @token.expiry,
49
- config: @config,
50
- id: @resource.id
51
- }
52
- @auth_params.merge!(oauth_registration: true) if @oauth_registration
53
- @auth_params
54
- end
55
-
56
- def get_resource_from_auth_hash # rubocop:disable Naming/AccessorMethodName
57
- # invitation_token = request.env.dig('omniauth.params', 'invitation_token')
58
- language = request.env.dig('omniauth.params', 'language')
59
-
60
- c_user = begin
61
- current_user
62
- rescue StandardError
63
- nil
64
- end
65
-
66
- identity_params = {
67
- auth: auth_hash,
68
- current_user: c_user,
69
- user_attrs: {
70
- defaults: {
71
- roles: Groovestack::Config::App.generate_config[:has_admins] ? [] : [Users::Roles::Role::ADMIN]
72
- },
73
- priority: {
74
- language: language
75
- }
76
- }
77
- }
78
-
79
- @resource = Identity.find_or_create_from_omniauth!(**identity_params).user
80
-
81
- @resource
82
- end
83
-
84
- def verified # rubocop:disable Metrics/AbcSize
85
- omniauth_success do
86
- set_token_in_cookie(@resource, @token)
87
-
88
- redirect_url = if redirect_options.present? || request.env['omniauth.origin'].split('?').size > 1
89
- DeviseTokenAuth::Url.generate(request.env['omniauth.origin'], redirect_options)
90
- else
91
- request.env['omniauth.origin'].split('?').first # get rid of occasional trailing ?
92
- end
93
-
94
- redirect_to redirect_url
95
-
96
- return
97
- end
98
- end
99
-
100
- def omniauth_failure
101
- @error = params[:message]
102
- # render_data_or_redirect('authFailure', omniauth_failure_error: @error)
103
-
104
- ::Groovestack::Base.notify_error('Groovestack::Auth::OmniauthCallbacksController.omniauth_failure', @error)
105
-
106
- data = { omniauth_failure_error: @error }.merge(redirect_options)
107
- redirect_to DeviseTokenAuth::Url.generate(session['omniauth.origin'], data)
108
- end
109
- end
110
- end
111
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Groovestack
4
- module Auth
5
- module SchemaPlugin
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- use GraphqlDevise::SchemaPlugin.new(
10
- query: Types::QueryType,
11
- mutation: Types::MutationType,
12
- resource_loaders: [
13
- GraphqlDevise::ResourceLoader.new(User)
14
- ]
15
- )
16
- end
17
- end
18
- end
19
- end
data/lib/identity.rb DELETED
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class Identity < ActiveRecord::Base
4
- belongs_to :user
5
-
6
- def self.find_or_create_from_omniauth!(auth:, current_user: nil, user_attrs: {}) # rubocop:disable Metrics/AbcSize
7
- where(provider: auth.provider, uid: auth.uid).first_or_create! do |identity|
8
- # TODO
9
- # possible cases
10
- # 1. user exists in the system (i.e. we found an email for them)
11
- # 2. user does not exist in the system (i.e. oauth email doesn't match anything in the system
12
-
13
- user = current_user || User.find_by(email: auth.info.email)
14
- user_attrs_to_assign = user_attrs[:priority] || {}
15
-
16
- if user.nil?
17
- user_attrs_to_assign = user_attrs_to_assign.merge(user_attrs[:defaults] || {})
18
- user = User.new
19
- end
20
-
21
- attrs = auth['info'].to_hash.slice(*user.attribute_names)
22
- user.assign_attributes(attrs.merge(user_attrs_to_assign))
23
-
24
- # user.skip_confirmation!
25
- user.save!
26
-
27
- identity.omniauth_data = auth
28
- identity.user = user
29
- end
30
- end
31
- end
data/lib/user.rb DELETED
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'jsonb_accessor'
4
-
5
- class User < ActiveRecord::Base
6
- include Users::Roles
7
-
8
- if defined?(Devise)
9
- extend ::Devise::Models
10
-
11
- # Include default devise modules. Others available are:
12
- # :confirmable, :lockable, :timeoutable
13
-
14
- devise :database_authenticatable, :registerable,
15
- :recoverable, :rememberable, :trackable, :validatable
16
- end
17
-
18
- has_many :identities, dependent: :destroy
19
-
20
- scope :fuzzysearch, ->(q) { where('name::text ilike ?', "%#{q}%".gsub(/\s/, '%').squeeze('%')) }
21
- scope :emailsearch, ->(qemail) { where('email::text ilike ?', "#{qemail}%") }
22
-
23
- # GraphqlDevise overrides (due to omniauthable relying on new Identity model)
24
- # NOTE: DeviseTokenAuth User.provider & User.uid columns are removed
25
- # TODO: can we remove this overrides?
26
-
27
- def provider
28
- # will probably need to pick from identities eventually
29
- nil
30
- end
31
-
32
- def uid
33
- nil
34
- end
35
-
36
- def email_provider?
37
- encrypted_password.present?
38
- end
39
-
40
- def build_auth_headers(token, client = 'default') # rubocop:disable Metrics/AbcSize
41
- # client may use expiry to prevent validation request if expired
42
- # must be cast as string or headers will break
43
- expiry = tokens[client]['expiry'] || tokens[client][:expiry]
44
- headers = {
45
- DeviseTokenAuth.headers_names[:'access-token'] => token,
46
- DeviseTokenAuth.headers_names[:'token-type'] => 'Bearer',
47
- DeviseTokenAuth.headers_names[:client] => client,
48
- DeviseTokenAuth.headers_names[:expiry] => expiry.to_s,
49
- DeviseTokenAuth.headers_names[:id] => id
50
- }
51
- headers.merge(build_bearer_token(headers))
52
- end
53
- end
data/lib/users/roles.rb DELETED
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Users
4
- module Roles
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- scope :with_roles, ->(roles) { jsonb_contains(:roles, roles) }
9
- scope :admins, -> { with_roles(:admin) }
10
-
11
- module Role
12
- ADMIN = 'admin'
13
- end
14
-
15
- ROLES = [
16
- Role::ADMIN
17
- ].freeze
18
-
19
- def add_roles(roles)
20
- self.roles |= roles
21
- end
22
-
23
- def add_role(role)
24
- add_roles([role])
25
- end
26
-
27
- def admin!
28
- add_role(User::Role::ADMIN)
29
- save!
30
- end
31
-
32
- def admin?
33
- roles.include? User::Role::ADMIN
34
- end
35
-
36
- # TODO: rname role?
37
- def has_role?(role) # rubocop:disable Naming/PredicateName
38
- roles.include? role.to_s
39
- end
40
- end
41
- end
42
- end