super_settings 0.0.0.rc1 → 0.0.1.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 +4 -4
- data/CHANGELOG.md +2 -1
- data/README.md +65 -62
- data/VERSION +1 -1
- data/config/routes.rb +3 -1
- data/lib/super_settings/application/api.js +8 -1
- data/lib/super_settings/application/helper.rb +2 -1
- data/lib/super_settings/application/index.html.erb +3 -3
- data/lib/super_settings/application/scripts.js +50 -12
- data/lib/super_settings/application/styles.css +5 -0
- data/lib/super_settings/application.rb +3 -1
- data/lib/super_settings/coerce.rb +6 -2
- data/lib/super_settings/configuration.rb +34 -15
- data/lib/super_settings/engine.rb +0 -5
- data/lib/super_settings/history_item.rb +10 -2
- data/lib/super_settings/local_cache.rb +14 -3
- data/lib/super_settings/{rack_middleware.rb → rack_application.rb} +66 -36
- data/lib/super_settings/rest_api.rb +98 -97
- data/lib/super_settings/setting.rb +96 -73
- data/lib/super_settings/storage/active_record_storage.rb +28 -10
- data/lib/super_settings/storage/http_storage.rb +7 -17
- data/lib/super_settings/storage/redis_storage.rb +20 -33
- data/lib/super_settings/storage/test_storage.rb +3 -10
- data/lib/super_settings/storage.rb +49 -24
- data/lib/super_settings.rb +33 -20
- data/super_settings.gemspec +1 -3
- metadata +4 -20
- data/lib/super_settings/encryption.rb +0 -76
- data/lib/tasks/super_settings.rake +0 -9
@@ -46,11 +46,6 @@ module SuperSettings
|
|
46
46
|
Setting.cache = (configuration.model.cache || Rails.cache)
|
47
47
|
Setting.storage = configuration.model.storage_class
|
48
48
|
|
49
|
-
if configuration.secret.present?
|
50
|
-
SuperSettings.secret = configuration.secret
|
51
|
-
configuration.secret = nil
|
52
|
-
end
|
53
|
-
|
54
49
|
if !SuperSettings.loaded?
|
55
50
|
begin
|
56
51
|
SuperSettings.load_settings
|
@@ -16,8 +16,16 @@ module SuperSettings
|
|
16
16
|
|
17
17
|
# The method could be overriden to change how the changed_by attribute is displayed.
|
18
18
|
# For instance, you could store a user id in the changed_by column and add an association
|
19
|
-
# on this model
|
20
|
-
#
|
19
|
+
# on this model.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# class SuperSettings::HistoryItem
|
23
|
+
# def changed_by_display
|
24
|
+
# user = User.find_by(id: changed_by) if changed_by
|
25
|
+
# user ? user.name : changed_by
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
21
29
|
# @return [String]
|
22
30
|
def changed_by_display
|
23
31
|
changed_by
|
@@ -20,7 +20,7 @@ module SuperSettings
|
|
20
20
|
# checked for changed settings at most this often.
|
21
21
|
attr_reader :refresh_interval
|
22
22
|
|
23
|
-
# @
|
23
|
+
# @param refresh_interval [Numeric] number of seconds to wait between checking for setting updates
|
24
24
|
def initialize(refresh_interval:)
|
25
25
|
@refresh_interval = refresh_interval
|
26
26
|
@lock = Mutex.new
|
@@ -65,7 +65,18 @@ module SuperSettings
|
|
65
65
|
end
|
66
66
|
|
67
67
|
# Return the setting as structured data. The keys will be split by the specified delimiter
|
68
|
-
# to create a nested hash
|
68
|
+
# to create a nested hash.
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# Setting with key "a.b.c" and value 1 becomes
|
72
|
+
#
|
73
|
+
# {
|
74
|
+
# "a" => {
|
75
|
+
# "b" => {
|
76
|
+
# "c" => 1
|
77
|
+
# }
|
78
|
+
# }
|
79
|
+
# }
|
69
80
|
#
|
70
81
|
# See SuperSettings.structured for more details.
|
71
82
|
def structured(key = nil, delimiter: ".", max_depth: nil)
|
@@ -282,7 +293,7 @@ module SuperSettings
|
|
282
293
|
|
283
294
|
# Recusive method for creating a nested hash from delimited keys.
|
284
295
|
def set_nested_hash_value(hash, key, value, current_depth, delimiter:, max_depth:)
|
285
|
-
key, sub_key = (max_depth && current_depth < max_depth ? [key, nil] : key.split(delimiter, 2))
|
296
|
+
key, sub_key = ((max_depth && current_depth < max_depth) ? [key, nil] : key.split(delimiter, 2))
|
286
297
|
if sub_key
|
287
298
|
sub_hash = hash[key]
|
288
299
|
unless sub_hash.is_a?(Hash)
|
@@ -8,22 +8,40 @@ module SuperSettings
|
|
8
8
|
# The routes for the API can be mounted under a common path prefix specified in the initializer.
|
9
9
|
#
|
10
10
|
# You must specify some kind of authentication to use this class by at least overriding the
|
11
|
-
#
|
11
|
+
# +authenticated?+ method in a subclass. How you do this is left up to you since you will most
|
12
12
|
# likely want to integrate in with how the rest of your application authenticates requests.
|
13
13
|
#
|
14
14
|
# You are also responsible for implementing any CSRF protection if your authentication method
|
15
15
|
# uses stateful requests (i.e. cookies or Basic auth where browser automatically include the
|
16
16
|
# credentials on every reqeust). There are other gems available that can be integrated into
|
17
17
|
# your middleware stack to provide this feature. If you need to inject meta elements into
|
18
|
-
# the page, you can do so with the
|
19
|
-
class
|
20
|
-
RESPONSE_HEADERS = {"
|
18
|
+
# the page, you can do so with the +add_to_head+ method.
|
19
|
+
class RackApplication
|
20
|
+
RESPONSE_HEADERS = {"content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache"}.freeze
|
21
21
|
|
22
22
|
# @param app [Object] Rack application or middleware for unhandled requests
|
23
|
-
# @param
|
24
|
-
|
23
|
+
# @param path_prefix [String] path prefix for the API routes.
|
24
|
+
# @yield Block to be evaluated on the instance to extend it's behavior. You can use
|
25
|
+
# this to define the access control methods rather than having to extend the class.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
#
|
29
|
+
# app = SuperSettings::RackApplication.new do
|
30
|
+
# def current_user(request)
|
31
|
+
# auth = request["HTTP_AUTHORIZATION"]
|
32
|
+
# token_match = auth&.match(/\ABearer:\s*(.*)/)
|
33
|
+
# token = token_match[1] if token_match
|
34
|
+
# User.identified_by(token)
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# def allow_write?(user)
|
38
|
+
# user.admin?
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
def initialize(app = nil, path_prefix = "/", &block)
|
25
42
|
@app = app
|
26
43
|
@path_prefix = path_prefix.to_s.chomp("/")
|
44
|
+
instance_eval(&block) if block
|
27
45
|
end
|
28
46
|
|
29
47
|
def call(env)
|
@@ -34,39 +52,45 @@ module SuperSettings
|
|
34
52
|
end
|
35
53
|
end
|
36
54
|
|
37
|
-
|
55
|
+
# Subclasses must override this method to return the current user object. This object will
|
56
|
+
# be passed to the authenticated?, allow_read?, allow_write?, and changed_by methods.
|
57
|
+
#
|
58
|
+
# @param request [Rack::Request] current request object
|
59
|
+
# @return [Object]
|
60
|
+
def current_user(request)
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
38
63
|
|
39
|
-
# Subclasses
|
40
|
-
#
|
64
|
+
# Subclasses can override this method to indicate if a user is authenticated. By default
|
65
|
+
# a request will be considered authenticated if the +current_user+ method returns a value.
|
66
|
+
#
|
67
|
+
# @param user [Object] the value returned by the +current_user+ method.
|
41
68
|
# @return [Boolean] true if the user is authenticated.
|
42
69
|
def authenticated?(user)
|
43
|
-
|
70
|
+
!!user
|
44
71
|
end
|
45
72
|
|
46
73
|
# Subclasses can override this method to indicate if the specified user is allowed to view settings.
|
47
|
-
#
|
74
|
+
# By default if a user is authenticated they will be able to read settings.
|
75
|
+
#
|
76
|
+
# @param user [Object] the value returned by the +current_user+ method.
|
48
77
|
# @return [Boolean] true if the user is can view settings.
|
49
78
|
def allow_read?(user)
|
50
79
|
true
|
51
80
|
end
|
52
81
|
|
53
82
|
# Subclasses can override this method to indicate if the specified user is allowed to change settings.
|
54
|
-
#
|
83
|
+
# By default if a user can read settings, then they will be able to write them as well.
|
84
|
+
#
|
85
|
+
# @param user [Object] the value returned by the +current_user+ method.
|
55
86
|
# @return [Boolean] true if the user is can change settings.
|
56
87
|
def allow_write?(user)
|
57
|
-
|
88
|
+
allow_read?(user)
|
58
89
|
end
|
59
90
|
|
60
|
-
# Subclasses can override this method to return the
|
61
|
-
# be
|
62
|
-
#
|
63
|
-
# @return [Object]
|
64
|
-
def current_user(request)
|
65
|
-
nil
|
66
|
-
end
|
67
|
-
|
68
|
-
# Subclasses can override this method to return the information about the current user that will
|
69
|
-
# be stored in the setting history when a setting is changed.
|
91
|
+
# Subclasses can override this method to return the information to record about the current user
|
92
|
+
# that will be stored in the setting history when a setting is changed.
|
93
|
+
#
|
70
94
|
# @return [String]
|
71
95
|
def changed_by(user)
|
72
96
|
nil
|
@@ -74,6 +98,7 @@ module SuperSettings
|
|
74
98
|
|
75
99
|
# Subclasses can override this method to return the path to an ERB file to use as the layout
|
76
100
|
# for the HTML application. The layout can use any of the methods defined in SuperSettings::Application::Helper.
|
101
|
+
#
|
77
102
|
# @return [String]
|
78
103
|
def layout
|
79
104
|
"layout.html.erb"
|
@@ -81,17 +106,18 @@ module SuperSettings
|
|
81
106
|
|
82
107
|
# Subclasses can override this method to add custom HTML to the <head> element in the HTML application.
|
83
108
|
# This can be used to add additional script or meta tags needed for CSRF protection, etc.
|
109
|
+
#
|
84
110
|
# @param request [Rack::Request] current reqeust object
|
85
111
|
# @return [String]
|
86
112
|
def add_to_head(request)
|
87
113
|
end
|
88
114
|
|
89
|
-
# Subclasses can override this method to
|
90
|
-
#
|
91
|
-
#
|
92
|
-
# @
|
93
|
-
|
94
|
-
|
115
|
+
# Subclasses can override this method to disable the web UI component of the application on only
|
116
|
+
# expose the REST API.
|
117
|
+
#
|
118
|
+
# @return [Boolean]
|
119
|
+
def web_ui_enabled?
|
120
|
+
true
|
95
121
|
end
|
96
122
|
|
97
123
|
private
|
@@ -100,7 +126,7 @@ module SuperSettings
|
|
100
126
|
request = Rack::Request.new(env)
|
101
127
|
path = request.path[@path_prefix.length, request.path.length]
|
102
128
|
if request.get?
|
103
|
-
if path == "/" || path == ""
|
129
|
+
if (path == "/" || path == "") && web_ui_enabled?
|
104
130
|
return handle_root_request(request)
|
105
131
|
elsif path == "/settings"
|
106
132
|
return handle_index_request(request)
|
@@ -118,18 +144,22 @@ module SuperSettings
|
|
118
144
|
return handle_update_request(request)
|
119
145
|
end
|
120
146
|
end
|
121
|
-
|
147
|
+
|
148
|
+
if @app
|
149
|
+
@app.call(env)
|
150
|
+
else
|
151
|
+
[404, {"content-type" => "text/plain"}, ["Not found"]]
|
152
|
+
end
|
122
153
|
end
|
123
154
|
|
124
155
|
def handle_root_request(request)
|
125
156
|
response = check_authorization(request, write_required: true) do |user|
|
126
|
-
[200, {"
|
157
|
+
[200, {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}, [Application.new(:default, add_to_head(request)).render("index.html.erb")]]
|
127
158
|
end
|
128
159
|
|
129
160
|
if [401, 403].include?(response.first)
|
130
|
-
|
131
|
-
|
132
|
-
response = [302, {"Location" => location}, []]
|
161
|
+
if SuperSettings.authentication_url
|
162
|
+
response = [302, {"location" => SuperSettings.authentication_url}, []]
|
133
163
|
end
|
134
164
|
end
|
135
165
|
|
@@ -196,7 +226,7 @@ module SuperSettings
|
|
196
226
|
end
|
197
227
|
|
198
228
|
def json_response(status, payload)
|
199
|
-
[status, RESPONSE_HEADERS, [payload.to_json]]
|
229
|
+
[status, RESPONSE_HEADERS.dup, [payload.to_json]]
|
200
230
|
end
|
201
231
|
|
202
232
|
def post_params(request)
|
@@ -7,22 +7,21 @@ module SuperSettings
|
|
7
7
|
class << self
|
8
8
|
# Get all settings sorted by key. This endpoint may be called with a REST GET request.
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# @example
|
11
|
+
# GET /
|
11
12
|
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# ]
|
25
|
-
# ```
|
13
|
+
# The response payload is:
|
14
|
+
# [
|
15
|
+
# {
|
16
|
+
# key: string,
|
17
|
+
# value: object,
|
18
|
+
# value_type: string,
|
19
|
+
# description string,
|
20
|
+
# created_at: iso8601 string,
|
21
|
+
# updated_at: iso8601 string
|
22
|
+
# },
|
23
|
+
# ...
|
24
|
+
# ]
|
26
25
|
def index
|
27
26
|
settings = Setting.active.sort_by(&:key)
|
28
27
|
{settings: settings.collect(&:as_json)}
|
@@ -30,53 +29,57 @@ module SuperSettings
|
|
30
29
|
|
31
30
|
# Get a setting by id.
|
32
31
|
#
|
33
|
-
#
|
32
|
+
# @example
|
33
|
+
# GET /setting
|
34
34
|
#
|
35
|
-
#
|
35
|
+
# Query parameters
|
36
36
|
#
|
37
|
-
#
|
37
|
+
# * key - setting key
|
38
38
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# }
|
49
|
-
# ```
|
39
|
+
# The response payload is:
|
40
|
+
# {
|
41
|
+
# key: string,
|
42
|
+
# value: object,
|
43
|
+
# value_type: string,
|
44
|
+
# description string,
|
45
|
+
# created_at: iso8601 string,
|
46
|
+
# updated_at: iso8601 string
|
47
|
+
# }
|
50
48
|
def show(key)
|
51
49
|
Setting.find_by_key(key)&.as_json
|
52
50
|
end
|
53
51
|
|
54
52
|
# The update operation uses a transaction to atomically update all settings.
|
55
53
|
#
|
56
|
-
#
|
54
|
+
# @example
|
55
|
+
# POST /settings
|
57
56
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
57
|
+
# The format of the parameters is an array of hashes with each setting identified by the key.
|
58
|
+
# The settings should include either "value" and "value_type" (and optionally "description") to
|
59
|
+
# insert or update a setting, or "deleted" to delete the setting.
|
61
60
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
61
|
+
# { settings: [
|
62
|
+
# {
|
63
|
+
# key: string,
|
64
|
+
# value: object,
|
65
|
+
# value_type: string,
|
66
|
+
# description: string,
|
67
|
+
# },
|
68
|
+
# {
|
69
|
+
# key: string,
|
70
|
+
# deleted: boolean,
|
71
|
+
# },
|
72
|
+
# ...
|
73
|
+
# ]
|
74
|
+
# }
|
75
|
+
#
|
76
|
+
# The response will be either
|
78
77
|
#
|
79
|
-
#
|
78
|
+
# {success: true}
|
79
|
+
#
|
80
|
+
# or
|
81
|
+
#
|
82
|
+
# {success: false, errors: {key => [string], ...}}
|
80
83
|
def update(settings_params, changed_by = nil)
|
81
84
|
all_valid, settings = Setting.bulk_update(Array(settings_params), changed_by)
|
82
85
|
if all_valid
|
@@ -94,41 +97,39 @@ module SuperSettings
|
|
94
97
|
|
95
98
|
# Return the history of the setting.
|
96
99
|
#
|
97
|
-
#
|
100
|
+
# @example
|
101
|
+
# GET /setting/history
|
98
102
|
#
|
99
|
-
#
|
103
|
+
# Query parameters
|
100
104
|
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
105
|
+
# * key - setting key
|
106
|
+
# * limit - number of history items to return
|
107
|
+
# * offset - index to start fetching items from (most recent items are first)
|
104
108
|
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
#
|
111
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
# next_page_params: hash
|
120
|
-
# }
|
121
|
-
# ```
|
109
|
+
# The response format is:
|
110
|
+
# {
|
111
|
+
# key: string,
|
112
|
+
# histories: [
|
113
|
+
# {
|
114
|
+
# value: object,
|
115
|
+
# changed_by: string,
|
116
|
+
# created_at: iso8601 string
|
117
|
+
# },
|
118
|
+
# ...
|
119
|
+
# ],
|
120
|
+
# previous_page_params: hash,
|
121
|
+
# next_page_params: hash
|
122
|
+
# }
|
122
123
|
def history(key, limit: nil, offset: 0)
|
123
124
|
setting = Setting.find_by_key(key)
|
124
125
|
return nil unless setting
|
125
126
|
|
126
127
|
offset = [offset.to_i, 0].max
|
127
128
|
limit = limit.to_i
|
128
|
-
fetch_limit = (limit > 0 ? limit + 1 : nil)
|
129
|
+
fetch_limit = ((limit > 0) ? limit + 1 : nil)
|
129
130
|
histories = setting.history(limit: fetch_limit, offset: offset)
|
130
131
|
|
131
|
-
payload = {key: setting.key
|
132
|
+
payload = {key: setting.key}
|
132
133
|
|
133
134
|
if limit > 0 && !histories.empty?
|
134
135
|
if offset > 0
|
@@ -153,38 +154,38 @@ module SuperSettings
|
|
153
154
|
|
154
155
|
# Return the timestamp of the most recently updated setting.
|
155
156
|
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
157
|
+
# @example
|
158
|
+
# GET /last_updated_at
|
159
|
+
# #
|
160
|
+
# The response payload is:
|
161
|
+
# {
|
162
|
+
# last_updated_at: iso8601 string
|
163
|
+
# }
|
162
164
|
def last_updated_at
|
163
165
|
{last_updated_at: Setting.last_updated_at.utc.iso8601}
|
164
166
|
end
|
165
167
|
|
166
168
|
# Return settings that have been updated since a specified timestamp.
|
167
169
|
#
|
168
|
-
#
|
170
|
+
# @example
|
171
|
+
# GET /updated_since
|
169
172
|
#
|
170
|
-
#
|
173
|
+
# Query parameters
|
171
174
|
#
|
172
|
-
#
|
175
|
+
# * time - iso8601 string
|
173
176
|
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
#
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
#
|
183
|
-
#
|
184
|
-
#
|
185
|
-
#
|
186
|
-
# ]
|
187
|
-
# ```
|
177
|
+
# The response payload is:
|
178
|
+
# [
|
179
|
+
# {
|
180
|
+
# key: string,
|
181
|
+
# value: object,
|
182
|
+
# value_type: string,
|
183
|
+
# description string,
|
184
|
+
# created_at: iso8601 string,
|
185
|
+
# updated_at: iso8601 string
|
186
|
+
# },
|
187
|
+
# ...
|
188
|
+
# ]
|
188
189
|
def updated_since(time)
|
189
190
|
time = Coerce.time(time)
|
190
191
|
settings = Setting.updated_since(time)
|