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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module ActionCable
6
+ module Connection
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ identified_by :current_resource
11
+ end
12
+
13
+ def connect
14
+ self.current_resource = find_verified_resource
15
+ end
16
+
17
+ private
18
+
19
+ def find_verified_resource
20
+ if (verified_user = env['warden'].user)
21
+ verified_user
22
+ else
23
+ reject_unauthorized_connection
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,111 @@
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: Core::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
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ def self.available_providers(ancestor: nil)
6
+ # ensure all providers are loaded
7
+ ::Groovestack::Auth::Providers.eager_load!
8
+
9
+ root = ancestor || ::Groovestack::Auth::Provider
10
+
11
+ root.descendants.select(&:available?)
12
+ end
13
+
14
+ def self.enabled_providers(ancestor: nil)
15
+ available_providers(ancestor: ancestor).select(&:enabled?)
16
+ end
17
+
18
+ def self.configured_providers(ancestor: nil)
19
+ enabled_providers(ancestor: ancestor).select(&:configured?)
20
+ end
21
+
22
+ def self.enabled_providers_sans_configuration(ancestor: nil)
23
+ enabled_providers(ancestor: ancestor) - configured_providers(ancestor: ancestor)
24
+ end
25
+
26
+ class Provider
27
+ def self.provider
28
+ const_defined?(:PROVIDER) ? self::PROVIDER : nil
29
+ end
30
+
31
+ def self.k
32
+ const_defined?(:K) ? self::K : provider
33
+ end
34
+
35
+ def self.available?
36
+ provider.present?
37
+ end
38
+
39
+ def self.enabled?
40
+ available? && ::Groovestack::Auth.disabled_providers.exclude?(provider)
41
+ end
42
+
43
+ def self.configured?
44
+ false
45
+ end
46
+
47
+ def self.as_json(keys = nil)
48
+ verbose = {
49
+ provider: provider,
50
+ k: k || provider
51
+ }
52
+
53
+ return verbose if keys.nil?
54
+
55
+ verbose.slice(*keys)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Providers
6
+ class Apple < OmniAuth
7
+ PROVIDER = :apple
8
+ REQUIRED_CREDENTIALS = %i[APPLE_CLIENT_ID APPLE_TEAM_ID APPLE_KEY_ID APPLE_PEM_CONTENT].freeze
9
+
10
+ def self.generate_omniauth_args
11
+ [
12
+ provider,
13
+ Rails.application.credentials.APPLE_CLIENT_ID,
14
+ '',
15
+ {
16
+ team_id: Rails.application.credentials.APPLE_TEAM_ID,
17
+ key_id: Rails.application.credentials.APPLE_KEY_ID,
18
+ pem: Rails.application.credentials.APPLE_PEM_CONTENT,
19
+ origin_param: 'return_to'
20
+ }
21
+ ]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Providers
6
+ class Email < Groovestack::Auth::Provider
7
+ PROVIDER = :email
8
+
9
+ def self.configured?
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Providers
6
+ class Google < OmniAuth
7
+ PROVIDER = :google_oauth2
8
+ K = :google
9
+ REQUIRED_CREDENTIALS = %i[GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET].freeze
10
+
11
+ def self.generate_omniauth_args
12
+ super << { name: k, origin_param: 'return_to' }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ module Providers
6
+ class OmniAuth < Groovestack::Auth::Provider
7
+ BASE_PATH = '/users/auth'
8
+
9
+ def self.required_credentials
10
+ const_defined?(:REQUIRED_CREDENTIALS) ? self::REQUIRED_CREDENTIALS : []
11
+ end
12
+
13
+ def self.required_credentials_present?
14
+ required_credentials.all? { |credential| Rails.application.credentials.send(credential).present? }
15
+ end
16
+
17
+ def self.configured?
18
+ required_credentials_present?
19
+ end
20
+
21
+ def self.generate_omniauth_args
22
+ # different providers require different arguments
23
+ [
24
+ provider,
25
+ *required_credentials.map { |c| Rails.application.credentials.send(c) }
26
+ ]
27
+ end
28
+
29
+ def self.path
30
+ "#{BASE_PATH}/#{k}"
31
+ end
32
+
33
+ def self.as_json(keys = nil)
34
+ verbose = super.merge({
35
+ required_credentials: required_credentials,
36
+ path: path,
37
+ generate_omniauth_args: generate_omniauth_args
38
+ })
39
+
40
+ return verbose if keys.nil?
41
+
42
+ verbose.slice(*keys)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'provider'
4
+
5
+ if defined?(Rails)
6
+ module Groovestack
7
+ module Auth
8
+ class Railtie < ::Rails::Engine
9
+ include ::Groovestack::Base::CoreRailtie
10
+
11
+ def dx_validations
12
+ [
13
+ {
14
+ eval: proc { raise unless defined?(::Groovestack::Base) },
15
+ message: "Error: 'core-base' gem is required, add it your your gemfile"
16
+ },
17
+ {
18
+ eval: proc { raise unless defined?(::Core::Config) },
19
+ message: "Error: 'core-config' gem is required, add it your your gemfile"
20
+ },
21
+ {
22
+ eval: proc {
23
+ raise if ::Groovestack::Auth.enabled_providers_sans_configuration.present?
24
+ },
25
+ message: missing_configuration_msg
26
+ }
27
+ ]
28
+ end
29
+
30
+ def providers_missing_configuration
31
+ ::Groovestack::Auth.enabled_providers_sans_configuration.map do |h|
32
+ "#{h.provider.to_s.titleize} - #{h.required_credentials.join(', ')}"
33
+ end
34
+ end
35
+
36
+ def missing_configuration_msg
37
+ msg = "\n\tWarning: enabled providers are missing required credentials:\n\t\t"
38
+ msg += providers_missing_configuration.join("\n\t\t")
39
+ "#{msg}\n\tAdd them to your credentials file or disable the providers by adding them to Groovestack::Auth.disabled_providers." # rubocop:disable Layout/LineLength
40
+ end
41
+
42
+ def module_description
43
+ avail = ::Groovestack::Auth.available_providers.map(&:k).map(&:to_s).map(&:titleize)
44
+ descr = "\n\tAvailable auth providers: #{avail.join(', ')}"
45
+ enabled = ::Groovestack::Auth.enabled_providers.map(&:k).map(&:to_s).map(&:titleize)
46
+ descr += "\n\tEnabled auth providers: #{enabled.join(', ')}"
47
+ descr
48
+ end
49
+
50
+ initializer :append_migrations do |app|
51
+ append_migrations app
52
+ end
53
+
54
+ initializer :append_initializers do |app|
55
+ append_initializers app
56
+ end
57
+
58
+ config.after_initialize do
59
+ after_init
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Groovestack
4
+ module Auth
5
+ VERSION = '0.1.1'
6
+ end
7
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'groovestack/base'
4
+
5
+ require 'graphql_devise' if defined?(GraphqlDevise)
6
+ require 'omniauth-google-oauth2'
7
+ require 'omniauth-apple'
8
+
9
+ require 'groovestack/auth/version'
10
+ require 'groovestack/auth/railtie' if defined?(Rails::Railtie)
11
+
12
+ if defined?(GraphqlDevise)
13
+ # add devise and devise_token_auth app/ dirs to load path
14
+ Dir[File.join(Gem::Specification.find_by_name('devise').gem_dir, 'app', '*')].each do |sub_dir|
15
+ $LOAD_PATH.push(sub_dir)
16
+ end
17
+ Dir[File.join(Gem::Specification.find_by_name('devise_token_auth').gem_dir, 'app', '*')].each do |sub_dir|
18
+ $LOAD_PATH.push(sub_dir)
19
+ end
20
+
21
+ unless defined?(ApplicationController)
22
+ class ApplicationController < ActionController::Base
23
+ end
24
+ end
25
+
26
+ unless defined?(DeviseTokenAuth::Concerns)
27
+ module DeviseTokenAuth
28
+ module Concerns
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ require 'users/roles'
35
+ require 'user'
36
+ require 'identity'
37
+
38
+ module GraphQL
39
+ module Identity
40
+ autoload :Type, 'graphql/identity/type'
41
+ autoload :Filter, 'graphql/identity/filter'
42
+ autoload :Queries, 'graphql/identity/queries'
43
+ autoload :Mutations, 'graphql/identity/mutations'
44
+ end
45
+
46
+ module User
47
+ autoload :Filter, 'graphql/user/filter'
48
+ autoload :Type, 'graphql/user/type'
49
+ autoload :Queries, 'graphql/user/queries'
50
+ autoload :Mutations, 'graphql/user/mutations'
51
+ end
52
+ end
53
+
54
+ module Groovestack
55
+ module Auth
56
+ autoload :Provider, 'groovestack/auth/provider'
57
+
58
+ if defined?(GraphqlDevise)
59
+ autoload :AuthenticatedApiController, 'groovestack/auth/authenticated_api_controller'
60
+ autoload :OmniauthCallbacksController, 'groovestack/auth/omniauth_callbacks_controller'
61
+ autoload :ActionCable, 'groovestack/auth/action_cable'
62
+ autoload :SchemaPlugin, 'groovestack/auth/schema_plugin'
63
+ end
64
+
65
+ module Providers
66
+ extend ActiveSupport::Autoload
67
+
68
+ eager_autoload do
69
+ autoload :Email, 'groovestack/auth/providers/email'
70
+ autoload :OmniAuth, 'groovestack/auth/providers/omni_auth'
71
+ autoload :Apple, 'groovestack/auth/providers/apple'
72
+ autoload :Google, 'groovestack/auth/providers/google'
73
+ end
74
+ end
75
+
76
+ extend Dry::Configurable
77
+
78
+ setting :disabled_providers, default: [], reader: true
79
+ end
80
+ end
81
+
82
+ require 'fabricators/user_fabricator' if defined?(Fabrication) && defined?(Faker)
83
+
84
+ # TODO: remove aliases after all core modules have been updated
85
+ # to reference Groovestack::Auth
86
+ module Core
87
+ module Auth
88
+ Provider = ::Groovestack::Auth::Provider
89
+
90
+ if defined?(GraphqlDevise)
91
+ AuthenticatedApiController = ::Groovestack::Auth::AuthenticatedApiController
92
+ OmniauthCallbacksController = ::Groovestack::Auth::OmniauthCallbacksController
93
+ ActionCable = ::Groovestack::Auth::ActionCable
94
+ SchemaPlugin = ::Groovestack::Auth::SchemaPlugin
95
+ end
96
+
97
+ module Providers
98
+ Email = ::Groovestack::Auth::Providers::Email
99
+ OmniAuth = ::Groovestack::Auth::Providers::OmniAuth
100
+ Apple = ::Groovestack::Auth::Providers::Apple
101
+ Google = ::Groovestack::Auth::Providers::Google
102
+ end
103
+
104
+ extend Dry::Configurable
105
+
106
+ setting :disabled_providers, default: [], reader: true
107
+ end
108
+ end
data/lib/identity.rb ADDED
@@ -0,0 +1,31 @@
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 ADDED
@@ -0,0 +1,53 @@
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
@@ -0,0 +1,42 @@
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