ibrain-core 0.1.0
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +75 -0
- data/Rakefile +7 -0
- data/app/controllers/concerns/ibrain_errors.rb +23 -0
- data/app/controllers/concerns/ibrain_handler.rb +42 -0
- data/app/controllers/ibrain/base_controller.rb +28 -0
- data/app/controllers/ibrain/graphql_controller.rb +55 -0
- data/app/graphql/ibrain/base_schema.rb +52 -0
- data/app/graphql/ibrain/extentions/default_value.rb +15 -0
- data/app/graphql/ibrain/interfaces/base_interface.rb +5 -0
- data/app/graphql/ibrain/interfaces/person_interface.rb +15 -0
- data/app/graphql/ibrain/interfaces/record_interface.rb +10 -0
- data/app/graphql/ibrain/lazy/base.rb +8 -0
- data/app/graphql/ibrain/loaders/association_loader.rb +61 -0
- data/app/graphql/ibrain/mutations/base_mutation.rb +35 -0
- data/app/graphql/ibrain/policies/base_policy.rb +45 -0
- data/app/graphql/ibrain/policies/graphql_policy.rb +8 -0
- data/app/graphql/ibrain/resolvers/base_aggregate.rb +9 -0
- data/app/graphql/ibrain/resolvers/base_resolver.rb +15 -0
- data/app/graphql/ibrain/types/aggregate_type.rb +9 -0
- data/app/graphql/ibrain/types/base_argument.rb +11 -0
- data/app/graphql/ibrain/types/base_connection.rb +16 -0
- data/app/graphql/ibrain/types/base_edge.rb +10 -0
- data/app/graphql/ibrain/types/base_enum.rb +8 -0
- data/app/graphql/ibrain/types/base_field.rb +15 -0
- data/app/graphql/ibrain/types/base_input_object.rb +9 -0
- data/app/graphql/ibrain/types/base_interface.rb +14 -0
- data/app/graphql/ibrain/types/base_node.rb +13 -0
- data/app/graphql/ibrain/types/base_object.rb +14 -0
- data/app/graphql/ibrain/types/base_query_type.rb +14 -0
- data/app/graphql/ibrain/types/base_scalar.rb +8 -0
- data/app/graphql/ibrain/types/base_union.rb +10 -0
- data/app/graphql/ibrain/types/filter_type.rb +8 -0
- data/app/graphql/ibrain/types/node_type.rb +11 -0
- data/app/graphql/ibrain/util/field_combiner.rb +13 -0
- data/app/graphql/ibrain/util/query_combiner.rb +13 -0
- data/app/graphql/mutations/insert_user.rb +18 -0
- data/app/models/ibrain/ability.rb +51 -0
- data/app/models/ibrain/application_record.rb +7 -0
- data/app/models/ibrain/base.rb +47 -0
- data/app/models/ibrain/concerns/ransackable_attributes.rb +22 -0
- data/app/models/ibrain/concerns/soft_deletable.rb +16 -0
- data/app/models/ibrain/concerns/user_api_authentication.rb +23 -0
- data/app/models/ibrain/concerns/user_methods.rb +25 -0
- data/app/models/ibrain/legacy_user.rb +19 -0
- data/app/models/ibrain/role.rb +14 -0
- data/app/models/ibrain/role_user.rb +18 -0
- data/config/initializers/friendly_id.rb +87 -0
- data/config/locales/en.yml +10 -0
- data/config/locales/jp.yml +10 -0
- data/config/locales/vi.yml +10 -0
- data/config/routes.rb +9 -0
- data/lib/generators/ibrain/graphql/core.rb +75 -0
- data/lib/generators/ibrain/graphql/mutation_generator.rb +58 -0
- data/lib/generators/ibrain/graphql/object_generator.rb +80 -0
- data/lib/generators/ibrain/graphql/resolver_generator.rb +33 -0
- data/lib/generators/ibrain/graphql/resolvers_generator.rb +59 -0
- data/lib/generators/ibrain/graphql/templates/aggregate.erb +10 -0
- data/lib/generators/ibrain/graphql/templates/mutation.erb +16 -0
- data/lib/generators/ibrain/graphql/templates/object.erb +11 -0
- data/lib/generators/ibrain/graphql/templates/resolver.erb +15 -0
- data/lib/generators/ibrain/graphql/templates/resolvers.erb +13 -0
- data/lib/generators/ibrain/graphql/type_generator.rb +101 -0
- data/lib/generators/ibrain/install/install_generator.rb +189 -0
- data/lib/generators/ibrain/install/templates/config/database.tt +23 -0
- data/lib/generators/ibrain/install/templates/config/initializers/cors.tt +25 -0
- data/lib/generators/ibrain/install/templates/config/initializers/ibrain.rb.tt +55 -0
- data/lib/generators/ibrain/install/templates/config/puma.tt +43 -0
- data/lib/generators/ibrain/install/templates/graphql/app_schema.rb.tt +4 -0
- data/lib/generators/ibrain/install/templates/graphql/types/mutation_type.rb.tt +10 -0
- data/lib/generators/ibrain/install/templates/graphql/types/query_type.rb.tt +13 -0
- data/lib/ibrain/app_configuration.rb +66 -0
- data/lib/ibrain/config.rb +5 -0
- data/lib/ibrain/core/class_constantizer.rb +41 -0
- data/lib/ibrain/core/controller_helpers/auth.rb +64 -0
- data/lib/ibrain/core/controller_helpers/current_host.rb +17 -0
- data/lib/ibrain/core/controller_helpers/response.rb +52 -0
- data/lib/ibrain/core/controller_helpers/strong_parameters.rb +21 -0
- data/lib/ibrain/core/engine.rb +16 -0
- data/lib/ibrain/core/environment.rb +17 -0
- data/lib/ibrain/core/environment_extension.rb +27 -0
- data/lib/ibrain/core/role_configuration.rb +72 -0
- data/lib/ibrain/core/validators/email.rb +23 -0
- data/lib/ibrain/core/version.rb +17 -0
- data/lib/ibrain/core/versioned_value.rb +73 -0
- data/lib/ibrain/core.rb +86 -0
- data/lib/ibrain/encryptor.rb +27 -0
- data/lib/ibrain/i18n.rb +17 -0
- data/lib/ibrain/logger.rb +23 -0
- data/lib/ibrain/permission_sets/base.rb +33 -0
- data/lib/ibrain/permission_sets/super_user.rb +11 -0
- data/lib/ibrain/permission_sets.rb +4 -0
- data/lib/ibrain/permitted_attributes.rb +26 -0
- data/lib/ibrain/preferences/configuration.rb +170 -0
- data/lib/ibrain/preferences/preferable.rb +183 -0
- data/lib/ibrain/preferences/preferable_class_methods.rb +140 -0
- data/lib/ibrain/user_class_handle.rb +29 -0
- data/lib/ibrain_core.rb +3 -0
- data/lib/tasks/ibrain/auto_annotate_models.rake +61 -0
- data/lib/tasks/ibrain/core_tasks.rake +5 -0
- data/lib/tasks/ibrain/ridgepole.rake +37 -0
- 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
|
data/lib/ibrain/i18n.rb
ADDED
|
@@ -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,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
|
data/lib/ibrain_core.rb
ADDED