super_settings 0.0.0.rc1
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/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +313 -0
- data/VERSION +1 -0
- data/app/helpers/super_settings/settings_helper.rb +32 -0
- data/app/views/layouts/super_settings/settings.html.erb +20 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210414004553_create_super_settings.rb +34 -0
- data/lib/super_settings/application/api.js +88 -0
- data/lib/super_settings/application/helper.rb +119 -0
- data/lib/super_settings/application/images/edit.svg +1 -0
- data/lib/super_settings/application/images/info.svg +1 -0
- data/lib/super_settings/application/images/plus.svg +1 -0
- data/lib/super_settings/application/images/slash.svg +1 -0
- data/lib/super_settings/application/images/trash.svg +1 -0
- data/lib/super_settings/application/index.html.erb +169 -0
- data/lib/super_settings/application/layout.html.erb +22 -0
- data/lib/super_settings/application/layout_styles.css +193 -0
- data/lib/super_settings/application/scripts.js +718 -0
- data/lib/super_settings/application/styles.css +122 -0
- data/lib/super_settings/application.rb +38 -0
- data/lib/super_settings/attributes.rb +24 -0
- data/lib/super_settings/coerce.rb +66 -0
- data/lib/super_settings/configuration.rb +144 -0
- data/lib/super_settings/controller_actions.rb +81 -0
- data/lib/super_settings/encryption.rb +76 -0
- data/lib/super_settings/engine.rb +70 -0
- data/lib/super_settings/history_item.rb +26 -0
- data/lib/super_settings/local_cache.rb +306 -0
- data/lib/super_settings/rack_middleware.rb +210 -0
- data/lib/super_settings/rest_api.rb +195 -0
- data/lib/super_settings/setting.rb +599 -0
- data/lib/super_settings/storage/active_record_storage.rb +123 -0
- data/lib/super_settings/storage/http_storage.rb +279 -0
- data/lib/super_settings/storage/redis_storage.rb +293 -0
- data/lib/super_settings/storage/test_storage.rb +158 -0
- data/lib/super_settings/storage.rb +254 -0
- data/lib/super_settings/version.rb +5 -0
- data/lib/super_settings.rb +213 -0
- data/lib/tasks/super_settings.rake +9 -0
- data/super_settings.gemspec +35 -0
- metadata +113 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
#settings-table td p {
|
2
|
+
margin-top: 0;
|
3
|
+
margin-bottom: 0.5rem;
|
4
|
+
}
|
5
|
+
|
6
|
+
#settings-table td p:last-of-type {
|
7
|
+
margin-bottom: 0;
|
8
|
+
}
|
9
|
+
|
10
|
+
#settings-table input[type=text], #settings-table input[type=number], #settings-table input[type=date], #settings-table input[type=time], #settings-table textarea {
|
11
|
+
width: 100%;
|
12
|
+
}
|
13
|
+
|
14
|
+
.super-settings-edit-row {
|
15
|
+
background-color: #f2fdf2 !important;
|
16
|
+
}
|
17
|
+
|
18
|
+
#settings-table tr[data-deleted] td {
|
19
|
+
background-color: #ffd1d8 !important;
|
20
|
+
color: darkred;
|
21
|
+
text-decoration: line-through;
|
22
|
+
}
|
23
|
+
|
24
|
+
#settings-table tr[data-newrecord] .js-show-history {
|
25
|
+
display: none;
|
26
|
+
}
|
27
|
+
|
28
|
+
.super-settings-key {
|
29
|
+
overflow-wrap: break-word;
|
30
|
+
max-width: 30rem;
|
31
|
+
min-width: 15rem;
|
32
|
+
}
|
33
|
+
|
34
|
+
.super-settings-value {
|
35
|
+
overflow-wrap: break-word;
|
36
|
+
max-width: 30rem;
|
37
|
+
min-width: 15rem;
|
38
|
+
}
|
39
|
+
|
40
|
+
.super-settings-value-type {
|
41
|
+
width: 7rem;
|
42
|
+
}
|
43
|
+
|
44
|
+
.super-settings-description {
|
45
|
+
min-width: 20rem;
|
46
|
+
}
|
47
|
+
|
48
|
+
.super-settings-controls {
|
49
|
+
width: 6rem;
|
50
|
+
white-space: nowrap;
|
51
|
+
text-align: right;
|
52
|
+
text-decoration: none !important;
|
53
|
+
}
|
54
|
+
|
55
|
+
.super-settings-history-key {
|
56
|
+
font-weight: normal;
|
57
|
+
color: royalblue;
|
58
|
+
}
|
59
|
+
|
60
|
+
.super-settings-modal {
|
61
|
+
z-index: 1000000000000;
|
62
|
+
display: none;
|
63
|
+
padding-top: 5rem;
|
64
|
+
position: fixed;
|
65
|
+
left: 0;
|
66
|
+
top: 0;
|
67
|
+
width: 100%;
|
68
|
+
height: 100%;
|
69
|
+
overflow: auto;
|
70
|
+
background-color: rgba(0,0,0,0.4);
|
71
|
+
}
|
72
|
+
|
73
|
+
.super-settings-modal-dialog {
|
74
|
+
margin: auto;
|
75
|
+
background-color: #fff;
|
76
|
+
position: relative;
|
77
|
+
outline: 0;
|
78
|
+
padding: 2em;
|
79
|
+
box-shadow: 3px 3px 7px 3px rgba(0, 0, 0, 0.6);
|
80
|
+
border-radius: 4px;
|
81
|
+
width: 80%;
|
82
|
+
height: 80%;
|
83
|
+
}
|
84
|
+
|
85
|
+
.super-settings-modal-content {
|
86
|
+
height: 100%;
|
87
|
+
overflow: auto;
|
88
|
+
}
|
89
|
+
|
90
|
+
.super-settings-modal-close {
|
91
|
+
display: block;
|
92
|
+
position: absolute;
|
93
|
+
top: 0;
|
94
|
+
right: 0;
|
95
|
+
border: 0;
|
96
|
+
padding: 1ex;
|
97
|
+
background-color: inherit;
|
98
|
+
font-size: 1.5rem;
|
99
|
+
font-weight: 500;
|
100
|
+
}
|
101
|
+
|
102
|
+
.super-settings-sr-only {
|
103
|
+
position: absolute;
|
104
|
+
width: 1px;
|
105
|
+
height: 1px;
|
106
|
+
padding: 0;
|
107
|
+
margin: -1px;
|
108
|
+
overflow: hidden;
|
109
|
+
clip: rect(0,0,0,0);
|
110
|
+
border: 0;
|
111
|
+
}
|
112
|
+
|
113
|
+
.super-settings-text-nowrap {
|
114
|
+
white-space: nowrap !important;
|
115
|
+
}
|
116
|
+
|
117
|
+
.super-settings-sticky-top {
|
118
|
+
position: sticky;
|
119
|
+
top: 0;
|
120
|
+
padding: 1rem 0;
|
121
|
+
background-color: white;
|
122
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "application/helper"
|
4
|
+
|
5
|
+
module SuperSettings
|
6
|
+
# Simple class for rendering ERB templates for the HTML application.
|
7
|
+
class Application
|
8
|
+
include Helper
|
9
|
+
|
10
|
+
# @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
|
11
|
+
# pass the symbol `:default` to use the default layout that ships with the gem.
|
12
|
+
# @param add_to_head [String] HTML code to add to the <head> element on the page.
|
13
|
+
def initialize(layout = nil, add_to_head = nil)
|
14
|
+
if layout
|
15
|
+
layout = File.expand_path(File.join("application", "layout.html.erb"), __dir__) if layout == :default
|
16
|
+
@layout = ERB.new(File.read(layout))
|
17
|
+
@add_to_head = add_to_head
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Render the specified ERB file in the lib/application directory distributed with the gem.
|
22
|
+
def render(erb_file)
|
23
|
+
template = ERB.new(File.read(File.expand_path(File.join("application", erb_file), __dir__)))
|
24
|
+
html = template.result(binding)
|
25
|
+
if @layout
|
26
|
+
render_layout { html }
|
27
|
+
else
|
28
|
+
html
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def render_layout
|
35
|
+
@layout.result(binding)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
# Interface to expose mass setting attributes on an object. Setting attributes with a
|
5
|
+
# hash will simply call the attribute writers for each key in the hash.
|
6
|
+
module Attributes
|
7
|
+
class UnknownAttributeError < StandardError
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(attributes = nil)
|
11
|
+
self.attributes = attributes if attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def attributes=(values)
|
15
|
+
values.each do |name, value|
|
16
|
+
if respond_to?("#{name}=", true)
|
17
|
+
send("#{name}=", value)
|
18
|
+
else
|
19
|
+
raise UnknownAttributeError.new("unknown attribute #{name.to_s.inspect} for #{self.class}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
# Utility functions for coercing values to other data types.
|
5
|
+
class Coerce
|
6
|
+
# rubocop:disable Lint/BooleanSymbol
|
7
|
+
FALSE_VALUES = [
|
8
|
+
false, 0,
|
9
|
+
"0", :"0",
|
10
|
+
"f", :f,
|
11
|
+
"F", :F,
|
12
|
+
"false", :false,
|
13
|
+
"FALSE", :FALSE,
|
14
|
+
"off", :off,
|
15
|
+
"OFF", :OFF
|
16
|
+
].to_set.freeze
|
17
|
+
# rubocop:enable Lint/BooleanSymbol
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Cast variations of booleans (i.e. "true", "false", 1, 0, etc.) to actual boolean objects.
|
21
|
+
# @param value [Object]
|
22
|
+
# @return [Boolean]
|
23
|
+
def boolean(value)
|
24
|
+
if value == false
|
25
|
+
false
|
26
|
+
elsif blank?(value)
|
27
|
+
nil
|
28
|
+
else
|
29
|
+
!FALSE_VALUES.include?(value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Cast a value to a Time object.
|
34
|
+
def time(value)
|
35
|
+
value = nil if value.nil? || value.to_s.empty?
|
36
|
+
return nil if value.nil?
|
37
|
+
time = if value.is_a?(Numeric)
|
38
|
+
Time.at(value)
|
39
|
+
elsif value.respond_to?(:to_time)
|
40
|
+
value.to_time
|
41
|
+
else
|
42
|
+
Time.parse(value.to_s)
|
43
|
+
end
|
44
|
+
if time.respond_to?(:in_time_zone) && Time.respond_to?(:zone)
|
45
|
+
time = time.in_time_zone(Time.zone)
|
46
|
+
end
|
47
|
+
time
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return true if the value is nil or empty.
|
51
|
+
def blank?(value)
|
52
|
+
return true if value.nil?
|
53
|
+
if value.respond_to?(:empty?)
|
54
|
+
value.empty?
|
55
|
+
else
|
56
|
+
value.to_s.empty?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return true if the value is not nil and not empty.
|
61
|
+
def present?(value)
|
62
|
+
!blank?(value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "singleton"
|
4
|
+
|
5
|
+
module SuperSettings
|
6
|
+
# Configuration for the gem when run as a Rails engine. Default values and behaviors
|
7
|
+
# on the controller and model can be overridden with the configuration.
|
8
|
+
#
|
9
|
+
# The configuration is a singleton instance.
|
10
|
+
class Configuration
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
# Configuration for the controller.
|
14
|
+
class Controller
|
15
|
+
# @api private
|
16
|
+
attr_reader :enhancement
|
17
|
+
|
18
|
+
# Superclass for the controller. This should normally be set to one of your existing
|
19
|
+
# base controller classes since these probably have authentication methods, etc. defined
|
20
|
+
# on them. If this is not defined, the superclass will be `SuperSettings::ApplicationController`.
|
21
|
+
# It can be set to either a class or a class name. Setting to a class name is preferrable
|
22
|
+
# since it will be compatible with class reloading in a development environment.
|
23
|
+
attr_writer :superclass
|
24
|
+
|
25
|
+
def superclass
|
26
|
+
if @superclass.is_a?(String)
|
27
|
+
@superclass.constantize
|
28
|
+
else
|
29
|
+
@superclass
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Optinal name of the application displayed in the view.
|
34
|
+
attr_accessor :application_name
|
35
|
+
|
36
|
+
# Optional mage URL for the application logo.
|
37
|
+
attr_accessor :application_logo
|
38
|
+
|
39
|
+
# Optional URL for a link back to the rest of the application.
|
40
|
+
attr_accessor :application_link
|
41
|
+
|
42
|
+
# Javascript to inject into the settings application HTML page. This can be used, for example,
|
43
|
+
# to set authorization credentials stored client side to access the settings API.
|
44
|
+
attr_accessor :javascript
|
45
|
+
|
46
|
+
# Enhance the controller. You can define methods or call controller class methods like
|
47
|
+
# `before_action`, etc. in the block. These will be applied to the engine controller.
|
48
|
+
# This is essentially the same a monkeypatching the controller class.
|
49
|
+
def enhance(&block)
|
50
|
+
@enhancement = block
|
51
|
+
end
|
52
|
+
|
53
|
+
# Define how the `changed_by` attibute on the setting history will be filled from the controller.
|
54
|
+
# The block will be evaluated in the context of the controller when the settings are changed.
|
55
|
+
# The value returned by the block will be stored in the changed_by attribute. For example, if
|
56
|
+
# your base controller class defines a method `current_user` and you'd like the name to be stored
|
57
|
+
# in the history, you could call `define_changed_by { current_user.name }`
|
58
|
+
def define_changed_by(&block)
|
59
|
+
@changed_by_block = block
|
60
|
+
end
|
61
|
+
|
62
|
+
# Return the value of `define_changed_by` block.
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
def changed_by(controller)
|
66
|
+
if defined?(@changed_by_block) && @changed_by_block
|
67
|
+
controller.instance_eval(&@changed_by_block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Configuration for the models.
|
73
|
+
class Model
|
74
|
+
# Specify the cache implementation to use for caching the last updated timestamp for reloading
|
75
|
+
# changed records. Defaults to `Rails.cache`
|
76
|
+
attr_accessor :cache
|
77
|
+
|
78
|
+
attr_writer :storage
|
79
|
+
|
80
|
+
# Specify the storage engine to use for persisting settings. The value can either be specified
|
81
|
+
# as a full class name or an underscored class name for a storage classed defined in the
|
82
|
+
# `SuperSettings::Storage` namespace. The default storage engine is `SuperSettings::Storage::ActiveRecord`.
|
83
|
+
def storage
|
84
|
+
if defined?(@storage) && @storage
|
85
|
+
@storage
|
86
|
+
else
|
87
|
+
:active_record
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [Class]
|
92
|
+
# @api private
|
93
|
+
def storage_class
|
94
|
+
if storage.is_a?(Class)
|
95
|
+
storage
|
96
|
+
else
|
97
|
+
class_name = storage.to_s.camelize
|
98
|
+
if Storage.const_defined?("#{class_name}Storage")
|
99
|
+
Storage.const_get("#{class_name}Storage")
|
100
|
+
else
|
101
|
+
class_name.constantize
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Return the model specific configuration object.
|
108
|
+
attr_reader :model
|
109
|
+
|
110
|
+
# Return the controller specific configuration object.
|
111
|
+
attr_reader :controller
|
112
|
+
|
113
|
+
# Set the secret used for encrypting secret settings. Defaults to the value of the
|
114
|
+
# SUPER_SETTINGS_SECRET environment variable. An array can be provided if you need to
|
115
|
+
# roll the secret with the first value being the current one.
|
116
|
+
attr_accessor :secret
|
117
|
+
|
118
|
+
# Set the number of seconds that settings will be cached locally before the database
|
119
|
+
# is checked for updates. Defaults to 5 seconds.
|
120
|
+
attr_accessor :refresh_interval
|
121
|
+
|
122
|
+
def initialize
|
123
|
+
@model = Model.new
|
124
|
+
@controller = Controller.new
|
125
|
+
end
|
126
|
+
|
127
|
+
# Defer the execution of a block that will be yielded to with the config object. This
|
128
|
+
# is needed in a Rails environment during initialization so that all the frameworks can
|
129
|
+
# load before loading the settings.
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def defer(&block)
|
133
|
+
@block = block
|
134
|
+
end
|
135
|
+
|
136
|
+
# Call the block deferred during initialization.
|
137
|
+
#
|
138
|
+
# @api private
|
139
|
+
def call
|
140
|
+
@block&.call(self)
|
141
|
+
@block = nil
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
|
5
|
+
module SuperSettings
|
6
|
+
# Module used to build the SuperSettings::SettingsController for Rails applications.
|
7
|
+
# This controller is defined at runtime since it is assumed that the superclass will
|
8
|
+
# be one of the application's own base controller classes since the application will
|
9
|
+
# want to define authentication and authorization criteria.
|
10
|
+
#
|
11
|
+
# The controller is built by extending the class defined by the Configuration object and
|
12
|
+
# then mixing in this module.
|
13
|
+
module ControllerActions
|
14
|
+
def self.included(base)
|
15
|
+
base.layout "super_settings/settings"
|
16
|
+
base.helper SettingsHelper
|
17
|
+
base.protect_from_forgery with: :exception, if: :protect_from_forgery?
|
18
|
+
end
|
19
|
+
|
20
|
+
# Render the HTML application for managing settings.
|
21
|
+
def root
|
22
|
+
html = SuperSettings::Application.new.render("index.html.erb")
|
23
|
+
render html: html.html_safe, layout: true
|
24
|
+
end
|
25
|
+
|
26
|
+
# API endpoint for getting active settings. See SuperSettings::RestAPI for details.
|
27
|
+
def index
|
28
|
+
render json: SuperSettings::RestAPI.index
|
29
|
+
end
|
30
|
+
|
31
|
+
# API endpoint for getting a setting. See SuperSettings::RestAPI for details.
|
32
|
+
def show
|
33
|
+
setting = SuperSettings::RestAPI.show(params[:key])
|
34
|
+
if setting
|
35
|
+
render json: setting
|
36
|
+
else
|
37
|
+
render json: nil, status: 404
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# API endpoint for updating settings. See SuperSettings::RestAPI for details.
|
42
|
+
def update
|
43
|
+
changed_by = Configuration.instance.controller.changed_by(self)
|
44
|
+
result = SuperSettings::RestAPI.update(params[:settings], changed_by)
|
45
|
+
if result[:success]
|
46
|
+
render json: result
|
47
|
+
else
|
48
|
+
render json: result, status: 422
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# API endpoint for getting the history of a setting. See SuperSettings::RestAPI for details.
|
53
|
+
def history
|
54
|
+
setting_history = SuperSettings::RestAPI.history(params[:key], offset: params[:offset], limit: params[:limit])
|
55
|
+
if setting_history
|
56
|
+
render json: setting_history
|
57
|
+
else
|
58
|
+
render json: nil, status: 404
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# API endpoint for getting the last time a setting was changed. See SuperSettings::RestAPI for details.
|
63
|
+
def last_updated_at
|
64
|
+
render json: SuperSettings::RestAPI.last_updated_at
|
65
|
+
end
|
66
|
+
|
67
|
+
# API endpoint for getting settings that have changed since specified time. See SuperSettings::RestAPI for details.
|
68
|
+
def updated_since
|
69
|
+
render json: SuperSettings::RestAPI.updated_since(params[:time])
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Return true if CSRF protection needs to be enabled for the request.
|
75
|
+
# By default it is only enabled on stateful requests that include Basic authorization
|
76
|
+
# or cookies in the request so that stateless REST API calls are allowed.
|
77
|
+
def protect_from_forgery?
|
78
|
+
request.cookies.present? || request.authorization.to_s.split(" ", 2).first&.match?(/\ABasic/i)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
module Encryption
|
5
|
+
SALT = "0c54a781"
|
6
|
+
private_constant :SALT
|
7
|
+
|
8
|
+
# Error thrown when the secret is invalid
|
9
|
+
class InvalidSecretError < StandardError
|
10
|
+
def initialize
|
11
|
+
super("Cannot decrypt. Invalid secret provided.")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Set the secret key used for encrypting secret values. If this is not set,
|
17
|
+
# the value will be loaded from the `SUPER_SETTINGS_SECRET` environment
|
18
|
+
# variable. If that value is not set, arguments will not be encrypted.
|
19
|
+
#
|
20
|
+
# You can set multiple secrets by passing an array if you need to roll your secrets.
|
21
|
+
# The left most value in the array will be used as the encryption secret, but
|
22
|
+
# all the values will be tried when decrypting. That way if you have existing keys
|
23
|
+
# that were encrypted with a different secret, you can still make it available
|
24
|
+
# when decrypting. If you are using the environment variable, separate the keys
|
25
|
+
# with spaces.
|
26
|
+
#
|
27
|
+
# @param value [String] One or more secrets to use for encrypting arguments.
|
28
|
+
# @return [void]
|
29
|
+
def secret=(value)
|
30
|
+
@encryptors = make_encryptors(value)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Encrypt a value for use with secret settings.
|
34
|
+
# @api private
|
35
|
+
def encrypt(value)
|
36
|
+
return nil if Coerce.blank?(value)
|
37
|
+
encryptor = encryptors.first
|
38
|
+
return value if encryptor.nil?
|
39
|
+
encryptor.encrypt(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Decrypt a value for use with secret settings.
|
43
|
+
# @api private
|
44
|
+
def decrypt(value)
|
45
|
+
return nil if Coerce.blank?(value)
|
46
|
+
return value if encryptors.empty? || encryptors == [nil]
|
47
|
+
encryptors.each do |encryptor|
|
48
|
+
begin
|
49
|
+
return encryptor.decrypt(value) if encryptor
|
50
|
+
rescue OpenSSL::Cipher::CipherError
|
51
|
+
# Not the right key, try the next one
|
52
|
+
end
|
53
|
+
end
|
54
|
+
raise InvalidSecretError
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean] true if the value is encrypted in the storage engine.
|
58
|
+
def encrypted?(value)
|
59
|
+
SecretKeys::Encryptor.encrypted?(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def encryptors
|
65
|
+
if !defined?(@encryptors) || @encryptors.empty?
|
66
|
+
@encryptors = make_encryptors(ENV["SUPER_SETTINGS_SECRET"].to_s.split)
|
67
|
+
end
|
68
|
+
@encryptors
|
69
|
+
end
|
70
|
+
|
71
|
+
def make_encryptors(secrets)
|
72
|
+
Array(secrets).map { |val| val.nil? ? nil : SecretKeys::Encryptor.from_password(val, SALT) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
# Engine that is loaded in a Rails environment. The engine will take care of applying any
|
5
|
+
# settings overriding behavior in the Configuration as well as eager loading the settings
|
6
|
+
# into memory.
|
7
|
+
class Engine < Rails::Engine
|
8
|
+
isolate_namespace ::SuperSettings
|
9
|
+
|
10
|
+
config.after_initialize do
|
11
|
+
# Call the deferred initialization block.
|
12
|
+
configuration = Configuration.instance
|
13
|
+
configuration.call
|
14
|
+
|
15
|
+
SuperSettings.refresh_interval = configuration.refresh_interval unless configuration.refresh_interval.nil?
|
16
|
+
|
17
|
+
reloader = if defined?(Rails.application.reloader.to_prepare)
|
18
|
+
Rails.application.reloader
|
19
|
+
elsif defined?(ActiveSupport::Reloader.to_prepare)
|
20
|
+
ActiveSupport::Reloader
|
21
|
+
elsif defined?(ActionDispatch::Reloader.to_prepare)
|
22
|
+
ActionDispatch::Reloader
|
23
|
+
end
|
24
|
+
|
25
|
+
create_controller = lambda do
|
26
|
+
klass = Class.new(configuration.controller.superclass || ::ApplicationController)
|
27
|
+
if defined?(SuperSettings::SettingsController)
|
28
|
+
SuperSettings.send(:remove_const, :SettingsController)
|
29
|
+
end
|
30
|
+
SuperSettings.const_set(:SettingsController, klass)
|
31
|
+
klass.include(ControllerActions)
|
32
|
+
if configuration.controller.enhancement
|
33
|
+
klass.class_eval(&configuration.controller.enhancement)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Setup the controller.
|
38
|
+
ActiveSupport.on_load(:action_controller) do
|
39
|
+
create_controller.call
|
40
|
+
if reloader && !Rails.configuration.cache_classes
|
41
|
+
reloader.to_prepare(&create_controller)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
model_load_block = proc do
|
46
|
+
Setting.cache = (configuration.model.cache || Rails.cache)
|
47
|
+
Setting.storage = configuration.model.storage_class
|
48
|
+
|
49
|
+
if configuration.secret.present?
|
50
|
+
SuperSettings.secret = configuration.secret
|
51
|
+
configuration.secret = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
if !SuperSettings.loaded?
|
55
|
+
begin
|
56
|
+
SuperSettings.load_settings
|
57
|
+
rescue => e
|
58
|
+
Rails.logger&.warn(e)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if configuration.model.storage.to_s == "active_record"
|
64
|
+
ActiveSupport.on_load(:active_record, &model_load_block)
|
65
|
+
else
|
66
|
+
model_load_block.call
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SuperSettings
|
4
|
+
# Model for each item in a setting's history. When a setting is changed, the system
|
5
|
+
# will track the value it is changed to, who it was changed by, and when.
|
6
|
+
class HistoryItem
|
7
|
+
include Attributes
|
8
|
+
|
9
|
+
attr_accessor :key, :value, :changed_by, :created_at
|
10
|
+
attr_writer :deleted
|
11
|
+
|
12
|
+
def deleted?
|
13
|
+
# Stupid strict mode...
|
14
|
+
!!(defined?(@deleted) && @deleted)
|
15
|
+
end
|
16
|
+
|
17
|
+
# The method could be overriden to change how the changed_by attribute is displayed.
|
18
|
+
# For instance, you could store a user id in the changed_by column and add an association
|
19
|
+
# on this model `belongs_to :user, class_name: "User", foreign_key: :changed_by` and then
|
20
|
+
# define this method as `user.name`.
|
21
|
+
# @return [String]
|
22
|
+
def changed_by_display
|
23
|
+
changed_by
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|