ibrain-core 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +75 -0
  4. data/Rakefile +7 -0
  5. data/app/controllers/concerns/ibrain_errors.rb +23 -0
  6. data/app/controllers/concerns/ibrain_handler.rb +42 -0
  7. data/app/controllers/ibrain/base_controller.rb +28 -0
  8. data/app/controllers/ibrain/graphql_controller.rb +55 -0
  9. data/app/graphql/ibrain/base_schema.rb +52 -0
  10. data/app/graphql/ibrain/extentions/default_value.rb +15 -0
  11. data/app/graphql/ibrain/interfaces/base_interface.rb +5 -0
  12. data/app/graphql/ibrain/interfaces/person_interface.rb +15 -0
  13. data/app/graphql/ibrain/interfaces/record_interface.rb +10 -0
  14. data/app/graphql/ibrain/lazy/base.rb +8 -0
  15. data/app/graphql/ibrain/loaders/association_loader.rb +61 -0
  16. data/app/graphql/ibrain/mutations/base_mutation.rb +35 -0
  17. data/app/graphql/ibrain/policies/base_policy.rb +45 -0
  18. data/app/graphql/ibrain/policies/graphql_policy.rb +8 -0
  19. data/app/graphql/ibrain/resolvers/base_aggregate.rb +9 -0
  20. data/app/graphql/ibrain/resolvers/base_resolver.rb +15 -0
  21. data/app/graphql/ibrain/types/aggregate_type.rb +9 -0
  22. data/app/graphql/ibrain/types/base_argument.rb +11 -0
  23. data/app/graphql/ibrain/types/base_connection.rb +16 -0
  24. data/app/graphql/ibrain/types/base_edge.rb +10 -0
  25. data/app/graphql/ibrain/types/base_enum.rb +8 -0
  26. data/app/graphql/ibrain/types/base_field.rb +15 -0
  27. data/app/graphql/ibrain/types/base_input_object.rb +9 -0
  28. data/app/graphql/ibrain/types/base_interface.rb +14 -0
  29. data/app/graphql/ibrain/types/base_node.rb +13 -0
  30. data/app/graphql/ibrain/types/base_object.rb +14 -0
  31. data/app/graphql/ibrain/types/base_query_type.rb +14 -0
  32. data/app/graphql/ibrain/types/base_scalar.rb +8 -0
  33. data/app/graphql/ibrain/types/base_union.rb +10 -0
  34. data/app/graphql/ibrain/types/filter_type.rb +8 -0
  35. data/app/graphql/ibrain/types/node_type.rb +11 -0
  36. data/app/graphql/ibrain/util/field_combiner.rb +13 -0
  37. data/app/graphql/ibrain/util/query_combiner.rb +13 -0
  38. data/app/graphql/mutations/insert_user.rb +18 -0
  39. data/app/models/ibrain/ability.rb +51 -0
  40. data/app/models/ibrain/application_record.rb +7 -0
  41. data/app/models/ibrain/base.rb +47 -0
  42. data/app/models/ibrain/concerns/ransackable_attributes.rb +22 -0
  43. data/app/models/ibrain/concerns/soft_deletable.rb +16 -0
  44. data/app/models/ibrain/concerns/user_api_authentication.rb +23 -0
  45. data/app/models/ibrain/concerns/user_methods.rb +25 -0
  46. data/app/models/ibrain/legacy_user.rb +19 -0
  47. data/app/models/ibrain/role.rb +14 -0
  48. data/app/models/ibrain/role_user.rb +18 -0
  49. data/config/initializers/friendly_id.rb +87 -0
  50. data/config/locales/en.yml +10 -0
  51. data/config/locales/jp.yml +10 -0
  52. data/config/locales/vi.yml +10 -0
  53. data/config/routes.rb +9 -0
  54. data/lib/generators/ibrain/graphql/core.rb +75 -0
  55. data/lib/generators/ibrain/graphql/mutation_generator.rb +58 -0
  56. data/lib/generators/ibrain/graphql/object_generator.rb +80 -0
  57. data/lib/generators/ibrain/graphql/resolver_generator.rb +33 -0
  58. data/lib/generators/ibrain/graphql/resolvers_generator.rb +59 -0
  59. data/lib/generators/ibrain/graphql/templates/aggregate.erb +10 -0
  60. data/lib/generators/ibrain/graphql/templates/mutation.erb +16 -0
  61. data/lib/generators/ibrain/graphql/templates/object.erb +11 -0
  62. data/lib/generators/ibrain/graphql/templates/resolver.erb +15 -0
  63. data/lib/generators/ibrain/graphql/templates/resolvers.erb +13 -0
  64. data/lib/generators/ibrain/graphql/type_generator.rb +101 -0
  65. data/lib/generators/ibrain/install/install_generator.rb +189 -0
  66. data/lib/generators/ibrain/install/templates/config/database.tt +23 -0
  67. data/lib/generators/ibrain/install/templates/config/initializers/cors.tt +25 -0
  68. data/lib/generators/ibrain/install/templates/config/initializers/ibrain.rb.tt +55 -0
  69. data/lib/generators/ibrain/install/templates/config/puma.tt +43 -0
  70. data/lib/generators/ibrain/install/templates/graphql/app_schema.rb.tt +4 -0
  71. data/lib/generators/ibrain/install/templates/graphql/types/mutation_type.rb.tt +10 -0
  72. data/lib/generators/ibrain/install/templates/graphql/types/query_type.rb.tt +13 -0
  73. data/lib/ibrain/app_configuration.rb +66 -0
  74. data/lib/ibrain/config.rb +5 -0
  75. data/lib/ibrain/core/class_constantizer.rb +41 -0
  76. data/lib/ibrain/core/controller_helpers/auth.rb +64 -0
  77. data/lib/ibrain/core/controller_helpers/current_host.rb +17 -0
  78. data/lib/ibrain/core/controller_helpers/response.rb +52 -0
  79. data/lib/ibrain/core/controller_helpers/strong_parameters.rb +21 -0
  80. data/lib/ibrain/core/engine.rb +16 -0
  81. data/lib/ibrain/core/environment.rb +17 -0
  82. data/lib/ibrain/core/environment_extension.rb +27 -0
  83. data/lib/ibrain/core/role_configuration.rb +72 -0
  84. data/lib/ibrain/core/validators/email.rb +23 -0
  85. data/lib/ibrain/core/version.rb +17 -0
  86. data/lib/ibrain/core/versioned_value.rb +73 -0
  87. data/lib/ibrain/core.rb +86 -0
  88. data/lib/ibrain/encryptor.rb +27 -0
  89. data/lib/ibrain/i18n.rb +17 -0
  90. data/lib/ibrain/logger.rb +23 -0
  91. data/lib/ibrain/permission_sets/base.rb +33 -0
  92. data/lib/ibrain/permission_sets/super_user.rb +11 -0
  93. data/lib/ibrain/permission_sets.rb +4 -0
  94. data/lib/ibrain/permitted_attributes.rb +26 -0
  95. data/lib/ibrain/preferences/configuration.rb +170 -0
  96. data/lib/ibrain/preferences/preferable.rb +183 -0
  97. data/lib/ibrain/preferences/preferable_class_methods.rb +140 -0
  98. data/lib/ibrain/user_class_handle.rb +29 -0
  99. data/lib/ibrain_core.rb +3 -0
  100. data/lib/tasks/ibrain/auto_annotate_models.rake +61 -0
  101. data/lib/tasks/ibrain/core_tasks.rake +5 -0
  102. data/lib/tasks/ibrain/ridgepole.rake +37 -0
  103. metadata +293 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ # Ibrain::Encryptor is a thin wrapper around ActiveSupport::MessageEncryptor.
5
+ class Encryptor
6
+ # @param key [String] the 256 bits signature key
7
+ def initialize(key = Ibrain::Config.ibrain_encryptor_key)
8
+ key = Rails.application.secrets.secret_key_base.byteslice(0..31) if key.blank?
9
+
10
+ @crypt = ActiveSupport::MessageEncryptor.new(key)
11
+ end
12
+
13
+ # Encrypt a value
14
+ # @param value [String] the value to encrypt
15
+ # @return [String] the encrypted value
16
+ def encrypt(value)
17
+ @crypt.encrypt_and_sign(value)
18
+ end
19
+
20
+ # Decrypt an encrypted value
21
+ # @param encrypted_value [String] the value to decrypt
22
+ # @return [String] the decrypted value
23
+ def decrypt(encrypted_value)
24
+ @crypt.decrypt_and_verify(encrypted_value)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n'
4
+
5
+ module Ibrain
6
+ def self.i18n_available_locales
7
+ I18n.available_locales.select do |locale|
8
+ I18n.t('ibrain.i18n.this_file_language', locale: locale, fallback: false, default: nil)
9
+ end
10
+ end
11
+
12
+ # This value is used as a count for the pluralization helpers related to I18n
13
+ # ex: Ibrain::Order.model_name.human(count: Ibrain::I18N_GENERIC_PLURAL)
14
+ # Related to Ibrain issue #1164, this is needed to avoid problems with
15
+ # some pluralization calculators
16
+ I18N_GENERIC_PLURAL = 2.1
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ class Logger
5
+ class << self
6
+ def info(message)
7
+ Rails.logger.info("[Ibrain] #{message}")
8
+ end
9
+
10
+ def warn(message)
11
+ Rails.logger.warn("[Ibrain] #{message}")
12
+ end
13
+
14
+ def debug(message)
15
+ Rails.logger.debug { "[Ibrain] #{message}" }
16
+ end
17
+
18
+ def error(message)
19
+ Rails.logger.error("[Ibrain] #{message}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ module PermissionSets
5
+ # This is the base class used for crafting permission sets.
6
+ #
7
+ # This is used by {Ibrain::RoleConfiguration} when adding custom behavior to {Ibrain::Ability}.
8
+ # See one of the subclasses for example structure such as {Ibrain::PermissionSets::UserDisplay}
9
+ #
10
+ # @see Ibrain::RoleConfiguration
11
+ # @see Ibrain::PermissionSets
12
+ class Base
13
+ # @param ability [CanCan::Ability]
14
+ # The ability that will be extended with the current permission set.
15
+ # The ability passed in must respond to #user
16
+ def initialize(ability)
17
+ @ability = ability
18
+ end
19
+
20
+ # Activate permissions on the ability. Put your can and cannot statements here.
21
+ # Must be overriden by subclasses
22
+ def activate!
23
+ raise NotImplementedError.new
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :ability
29
+
30
+ delegate :can, :cannot, :user, to: :ability
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ module PermissionSets
5
+ class SuperUser < PermissionSets::Base
6
+ def activate!
7
+ can :manage, :all
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ibrain/permission_sets/base'
4
+ require 'ibrain/permission_sets/super_user'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ # Ibrain::PermittedAttributes contains the attributes permitted through strong
5
+ # params in various controllers in the frontend. Extensions and stores that
6
+ # need additional params to be accepted can mutate these arrays to add them.
7
+ module PermittedAttributes
8
+ ATTRIBUTES = [
9
+ :address_attributes,
10
+ :user_attributes
11
+ ]
12
+
13
+ mattr_reader(*ATTRIBUTES)
14
+
15
+ @@address_attributes = [
16
+ :id, :name, :address1, :address2, :city, :country_id, :state_id,
17
+ :zipcode, :phone, :state_name, :province_id, :ward_id, :district_id
18
+ ]
19
+
20
+ # Intentionally leaving off email here to prevent privilege escalation
21
+ # by changing a user with higher priveleges' email to one a lower-priveleged
22
+ # admin owns. Creating a user with an email is handled separate at the
23
+ # controller level.
24
+ @@user_attributes = [:name, :email, :provider, :uid, :first_name, :last_name, :password, :password_confirmation]
25
+ end
26
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ibrain/core/versioned_value'
4
+ require 'ibrain/preferences/preferable'
5
+
6
+ module Ibrain::Preferences
7
+ # This takes the preferrable methods and adds some
8
+ # syntatic sugar to access the preferences
9
+ #
10
+ # class App < Configuration
11
+ # preference :color, :string
12
+ # end
13
+ #
14
+ # a = App.new
15
+ #
16
+ # Provides the following setters:
17
+ #
18
+ # a.color = :blue
19
+ # a[:color] = :blue
20
+ # a.set color: :blue
21
+ # a.preferred_color = :blue
22
+ #
23
+ # and the following getters:
24
+ #
25
+ # a.color
26
+ # a[:color]
27
+ # a.get :color
28
+ # a.preferred_color
29
+ #
30
+ class Configuration
31
+ include Ibrain::Preferences::Preferable
32
+
33
+ # @!attribute [r] loaded_defaults
34
+ # @return [String]
35
+ # Some configuration defaults can be added or changed when a new Ibrain
36
+ # version is released. Setting this to an older Ibrain version allows keeping
37
+ # backward compatibility until the application code is updated to the new
38
+ # defaults. Set via {#load_defaults}
39
+ attr_reader :loaded_defaults
40
+
41
+ attr_reader :load_defaults_called
42
+
43
+ def initialize
44
+ @loaded_defaults = Ibrain.ibrain_version
45
+ @load_defaults_called = false
46
+ end
47
+
48
+ # @param [String] Ibrain version from which take defaults when not
49
+ # overriden.
50
+ # @see #load_defaults
51
+ def load_defaults(version)
52
+ @loaded_defaults = version
53
+ @load_defaults_called = true
54
+ reset
55
+ end
56
+
57
+ def check_load_defaults_called(instance_constant_name = nil)
58
+ return if load_defaults_called || !Ibrain::Core.has_install_generator_been_run?
59
+
60
+ target_name = instance_constant_name || "#{self.class.name}.new"
61
+ Ibrain::Deprecation.warn <<~MSG
62
+ It's recommended that you explicitly load the default configuration for
63
+ your current Ibrain version. You can do it by adding the following call
64
+ to your Ibrain initializer within the #{target_name} block:
65
+
66
+ config.load_defaults('#{Ibrain.ibrain_version}')
67
+
68
+ MSG
69
+ end
70
+
71
+ # @yield [config] Yields this configuration object to a block
72
+ def configure
73
+ yield(self)
74
+ end
75
+
76
+ # @!attribute preference_store
77
+ # Storage method for preferences.
78
+ attr_writer :preference_store
79
+
80
+ def preference_store
81
+ @preference_store ||= default_preferences
82
+ end
83
+
84
+ # Replace the default legacy preference store, which stores preferences in
85
+ # the Ibrain_preferences table, with a plain in memory hash. This is faster
86
+ # and less error prone.
87
+ #
88
+ # This will set all preferences to their default values.
89
+ #
90
+ # These won't be loaded from or persisted to the database, so any desired
91
+ # changes must be made each time the application is started, such as in an
92
+ # initializer.
93
+ def use_static_preferences!
94
+ @preference_store = default_preferences
95
+ end
96
+
97
+ # Replace the new static preference store with the legacy store which
98
+ # fetches preferences from the DB.
99
+ def use_legacy_db_preferences!
100
+ @preference_store = ScopedStore.new(self.class.name.underscore)
101
+ end
102
+
103
+ alias_method :preferences, :preference_store
104
+
105
+ # Reset all preferences to their default values.
106
+ def reset
107
+ set(default_preferences)
108
+ end
109
+
110
+ alias :[] :get_preference
111
+ alias :[]= :set_preference
112
+
113
+ alias :get :get_preference
114
+
115
+ # @param preferences [Hash] a hash of preferences to set
116
+ def set(preferences)
117
+ preferences.each do |name, value|
118
+ set_preference name, value
119
+ end
120
+ end
121
+
122
+ # Generates a different preference default depending on {#version_defaults}
123
+ #
124
+ # This method is meant to be used in the `default:` keyword argument for
125
+ # {.preference}. For instance, in the example, `foo`'s default was `true`
126
+ # until version 3.0.0.alpha, when it became `false`:
127
+ #
128
+ # @example
129
+ # preference :foo, :boolean, default: by_version(true, "3.0.0.alpha" => false)
130
+ #
131
+ # @see #loaded_defaults
132
+ # @see Ibrain::Core::VersionedValue
133
+ def self.by_version(*args)
134
+ proc do |loaded_defaults|
135
+ Ibrain::Core::VersionedValue.new(*args).call(loaded_defaults)
136
+ end
137
+ end
138
+
139
+ def self.preference(name, type, options = {})
140
+ super
141
+ alias_method name.to_s, "preferred_#{name}"
142
+ alias_method "#{name}=", "preferred_#{name}="
143
+ end
144
+
145
+ def self.class_name_attribute(name, default:)
146
+ ivar = :"@#{name}"
147
+
148
+ define_method("#{name}=") do |class_name|
149
+ # If this is a named class constant, we should store it as a string to
150
+ # allow code reloading.
151
+ class_name = class_name.name if class_name.is_a?(Class) && class_name.name
152
+
153
+ instance_variable_set(ivar, class_name)
154
+ end
155
+
156
+ define_method(name) do
157
+ class_name = instance_variable_get(ivar)
158
+ class_name ||= default
159
+ class_name = class_name.constantize if class_name.is_a?(String)
160
+ class_name
161
+ end
162
+ end
163
+
164
+ private
165
+
166
+ def context_for_default
167
+ [loaded_defaults]
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ibrain/preferences/preferable_class_methods'
4
+ require 'active_support/concern'
5
+ require 'active_support/core_ext/hash/keys'
6
+
7
+ module Ibrain
8
+ module Preferences
9
+ # Preferable allows defining preference accessor methods.
10
+ #
11
+ # A class including Preferable must implement #preferences which should return
12
+ # an object responding to .fetch(key), []=(key, val), and .delete(key).
13
+ #
14
+ # It may also define a `#context_for_default` method. It should return an
15
+ # array with the arguments to be provided to a proc used as the `default:`
16
+ # keyword for a preference.
17
+ #
18
+ # The generated writer method performs typecasting before assignment into the
19
+ # preferences object.
20
+ #
21
+ # Examples:
22
+ #
23
+ # # Ibrain::Base includes Preferable and defines preferences as a serialized
24
+ # # column.
25
+ # class Settings < Ibrain::Base
26
+ # preference :color, :string, default: 'red'
27
+ # preference :temperature, :integer, default: 21
28
+ # end
29
+ #
30
+ # s = Settings.new
31
+ # s.preferred_color # => 'red'
32
+ # s.preferred_temperature # => 21
33
+ #
34
+ # s.preferred_color = 'blue'
35
+ # s.preferred_color # => 'blue'
36
+ #
37
+ # # Typecasting is performed on assignment
38
+ # s.preferred_temperature = '24'
39
+ # s.preferred_color # => 24
40
+ #
41
+ # # Modifications have been made to the .preferences hash
42
+ # s.preferences #=> {color: 'blue', temperature: 24}
43
+ #
44
+ # # Save the changes. All handled by activerecord
45
+ # s.save!
46
+ #
47
+ # Each preference gets rendered as a form field in Ibrain backend.
48
+ #
49
+ # As not all supported preference types are representable as a form field, only
50
+ # some of them get rendered per default. Arrays and Hashes for instance are
51
+ # supported preference field types, but do not represent well as a form field.
52
+ #
53
+ # Overwrite +allowed_admin_form_preference_types+ in your class if you want to
54
+ # provide more fields. If you do so, you also need to provide a preference field
55
+ # partial that lives in:
56
+ #
57
+ # +app/views/ibrain/admin/shared/preference_fields/+
58
+ #
59
+ module Preferable
60
+ extend ActiveSupport::Concern
61
+
62
+ included do
63
+ extend Ibrain::Preferences::PreferableClassMethods
64
+ end
65
+
66
+ # Get a preference
67
+ # @param name [#to_sym] name of preference
68
+ # @return [Object] The value of preference +name+
69
+ def get_preference(name)
70
+ has_preference! name
71
+ send self.class.preference_getter_method(name)
72
+ end
73
+
74
+ # Set a preference
75
+ # @param name [#to_sym] name of preference
76
+ # @param value [Object] new value for preference +name+
77
+ def set_preference(name, value)
78
+ has_preference! name
79
+ send self.class.preference_setter_method(name), value
80
+ end
81
+
82
+ # @param name [#to_sym] name of preference
83
+ # @return [Symbol] The type of preference +name+
84
+ def preference_type(name)
85
+ has_preference! name
86
+ send self.class.preference_type_getter_method(name)
87
+ end
88
+
89
+ # @param name [#to_sym] name of preference
90
+ # @return [Object] The default for preference +name+
91
+ def preference_default(name)
92
+ has_preference! name
93
+ send self.class.preference_default_getter_method(name)
94
+ end
95
+
96
+ # Raises an exception if the +name+ preference is not defined on this class
97
+ # @param name [#to_sym] name of preference
98
+ def has_preference!(name)
99
+ raise NoMethodError.new "#{name} preference not defined" unless has_preference? name
100
+ end
101
+
102
+ # @param name [#to_sym] name of preference
103
+ # @return [Boolean] if preference exists on this class
104
+ def has_preference?(name)
105
+ defined_preferences.include?(name.to_sym)
106
+ end
107
+
108
+ # @return [Array<Symbol>] All preferences defined on this class
109
+ def defined_preferences
110
+ self.class.defined_preferences
111
+ end
112
+
113
+ # @return [Hash{Symbol => Object}] Default for all preferences defined on this class
114
+ def default_preferences
115
+ defined_preferences.index_with do |preference|
116
+ preference_default(preference)
117
+ end
118
+ end
119
+
120
+ # Preference names representable as form fields in Ibrain backend
121
+ #
122
+ # Not all preferences are representable as a form field.
123
+ #
124
+ # Arrays and Hashes for instance are supported preference field types,
125
+ # but do not represent well as a form field.
126
+ #
127
+ # As these kind of preferences are mostly developer facing
128
+ # and not admin facing we should not render them.
129
+ #
130
+ # Overwrite +allowed_admin_form_preference_types+ in your class that
131
+ # includes +Ibrain::Preferable+ if you want to provide more fields.
132
+ # If you do so, you also need to provide a preference field partial
133
+ # that lives in:
134
+ #
135
+ # +app/views/ibrain/admin/shared/preference_fields/+
136
+ #
137
+ # @return [Array]
138
+
139
+ private
140
+
141
+ def convert_preference_value(value, type, preference_encryptor = nil)
142
+ return nil if value.nil?
143
+
144
+ case type
145
+ when :string, :text, :password
146
+ value.to_s
147
+ when :encrypted_string
148
+ preference_encryptor.encrypt(value.to_s)
149
+ when :decimal
150
+ begin
151
+ value.to_s.to_d
152
+ rescue ArgumentError
153
+ BigDecimal('0')
154
+ end
155
+ when :integer
156
+ value.to_i
157
+ when :boolean
158
+ if !value ||
159
+ value.to_s =~ /\A(f|false|0|^)\Z/i ||
160
+ (value.respond_to?(:empty?) && value.empty?)
161
+ false
162
+ else
163
+ true
164
+ end
165
+ when :array
166
+ raise TypeError, "Array expected got #{value.inspect}" unless value.is_a?(Array)
167
+
168
+ value
169
+ when :hash
170
+ raise TypeError, "Hash expected got #{value.inspect}" unless value.is_a?(Hash)
171
+
172
+ value
173
+ else
174
+ value
175
+ end
176
+ end
177
+
178
+ def context_for_default
179
+ [].freeze
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'ibrain/encryptor'
5
+
6
+ module Ibrain::Preferences
7
+ module PreferableClassMethods
8
+ DEFAULT_ADMIN_FORM_PREFERENCE_TYPES = %i(
9
+ boolean
10
+ decimal
11
+ integer
12
+ password
13
+ string
14
+ text
15
+ encrypted_string
16
+ )
17
+
18
+ def defined_preferences
19
+ []
20
+ end
21
+
22
+ def preference(name, type, options = {})
23
+ options.assert_valid_keys(:default, :encryption_key)
24
+
25
+ if type == :encrypted_string
26
+ preference_encryptor = preference_encryptor(options)
27
+ options[:default] = preference_encryptor.encrypt(options[:default])
28
+ end
29
+
30
+ default = begin
31
+ given = options[:default]
32
+ if self <= Ibrain::Preferences::Configuration &&
33
+ given.is_a?(Proc) &&
34
+ given.lambda? &&
35
+ given.arity.zero?
36
+ Ibrain::Logger.warn <<~MSG
37
+ The arity of a proc given as the default for a preference
38
+ has changed from 0 to 1 on Ibrain 3.1. The Ibrain
39
+ version for the loaded preference defaults is given as the
40
+ proc's argument from this point on.
41
+
42
+ If you don't need to return a different default value
43
+ depending on the loaded Ibrain version, you can change
44
+ the proc so that it doesn't have lambda semantics (lambdas
45
+ raise when extra arguments are supplied, while raw procs
46
+ don't). E.g.:
47
+
48
+ preference :foo, :string, default: proc { true }
49
+
50
+ If you want to branch on the provided Ibrain version, you can do like the following:
51
+
52
+ preference :foo, :string, default: by_version(true, "3.2.0" => false)
53
+
54
+ MSG
55
+ ->(_default_context) { given.call }
56
+ elsif given.is_a?(Proc)
57
+ given
58
+ else
59
+ proc { given }
60
+ end
61
+ end
62
+
63
+ # The defined preferences on a class are all those defined directly on
64
+ # that class as well as those defined on ancestors.
65
+ # We store these as a class instance variable on each class which has a
66
+ # preference. super() collects preferences defined on ancestors.
67
+ singleton_preferences = (@defined_singleton_preferences ||= [])
68
+ singleton_preferences << name.to_sym
69
+
70
+ define_singleton_method :defined_preferences do
71
+ super() + singleton_preferences
72
+ end
73
+
74
+ # cache_key will be nil for new objects, then if we check if there
75
+ # is a pending preference before going to default
76
+ define_method preference_getter_method(name) do
77
+ value = preferences.fetch(name) do
78
+ default.call(*context_for_default)
79
+ end
80
+ value = preference_encryptor.decrypt(value) if preference_encryptor.present?
81
+ value
82
+ end
83
+
84
+ define_method preference_setter_method(name) do |value|
85
+ value = convert_preference_value(value, type, preference_encryptor)
86
+ preferences[name] = value
87
+
88
+ # If this is an activerecord object, we need to inform
89
+ # ActiveRecord::Dirty that this value has changed, since this is an
90
+ # in-place update to the preferences hash.
91
+ preferences_will_change! if respond_to?(:preferences_will_change!)
92
+ end
93
+
94
+ define_method preference_default_getter_method(name) do
95
+ default.call(*context_for_default)
96
+ end
97
+
98
+ define_method preference_type_getter_method(name) do
99
+ type
100
+ end
101
+ end
102
+
103
+ def preference_getter_method(name)
104
+ "preferred_#{name}".to_sym
105
+ end
106
+
107
+ def preference_setter_method(name)
108
+ "preferred_#{name}=".to_sym
109
+ end
110
+
111
+ def preference_default_getter_method(name)
112
+ "preferred_#{name}_default".to_sym
113
+ end
114
+
115
+ def preference_type_getter_method(name)
116
+ "preferred_#{name}_type".to_sym
117
+ end
118
+
119
+ def preference_encryptor(options)
120
+ key = options[:encryption_key] ||
121
+ ENV['IBRAIN_PREFERENCES_MASTER_KEY'] ||
122
+ Rails.application.credentials.secret_key_base
123
+
124
+ Ibrain::Encryptor.new(key)
125
+ end
126
+
127
+ # List of preference types allowed as form fields in the Ibrain admin
128
+ #
129
+ # Overwrite this method in your class that includes +Ibrain::Preferable+
130
+ # if you want to provide more fields. If you do so, you also need to provide
131
+ # a preference field partial that lives in:
132
+ #
133
+ # +app/views/Ibrain/admin/shared/preference_fields/+
134
+ #
135
+ # @return [Array]
136
+ def allowed_admin_form_preference_types
137
+ DEFAULT_ADMIN_FORM_PREFERENCE_TYPES
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibrain
4
+ # Configuration point for User model implementation.
5
+ #
6
+ # `Ibrain::UserClassHandle` allows you to configure your own implementation of a
7
+ # User class or use an extension like `ibrain-auth`.
8
+ #
9
+ # @note Placeholder for name of Ibrain.user_class to ensure later evaluation at
10
+ # runtime.
11
+ #
12
+ # Unfortunately, it is possible for classes to get loaded before
13
+ # Ibrain.user_class has been set in the initializer. As a result, they end up
14
+ # with class_name: "" in their association definitions. For obvious reasons,
15
+ # that doesn't work.
16
+ #
17
+ # For now, Rails does not call to_s on the instance passed in until runtime.
18
+ # So this little hack provides a wrapper around Ibrain.user_class so that we
19
+ # can basically lazy-evaluate it. Yay! Problem solved forever.
20
+ class UserClassHandle
21
+ # @return [String] the name of the user class as a string.
22
+ # @raise [RuntimeError] if Ibrain.user_class is nil
23
+ def to_s
24
+ fail "'Ibrain.user_class' has not been set yet." unless Ibrain.user_class
25
+
26
+ "::#{Ibrain.user_class}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ibrain/core'