super_settings 0.0.0.rc1 → 0.0.1.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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)
|