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.
@@ -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 `belongs_to :user, class_name: "User", foreign_key: :changed_by` and then
20
- # define this method as `user.name`.
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
- # @parem refresh_interval [Numeric] number of seconds to wait between checking for setting updates
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 (i.e. "a.b.c" = 1 becomes `{"a" => {"b" => {"c" => 1}}}`).
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
- # `authenticated?` method in a subclass. How you do this is left up to you since you will most
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 `add_to_head` method.
19
- class RackMiddleware
20
- RESPONSE_HEADERS = {"Content-Type" => "application/json; charset=utf-8"}.freeze
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 prefix [String] path prefix for the API routes.
24
- def initialize(app, path_prefix = "/")
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
- protected
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 must implement this method.
40
- # @param user [Object] the value returned by the `current_user` method.
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
- raise NotImplementedError
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
- # @param user [Object] the value returned by the `current_user` method.
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
- # @param user [Object] the value returned by the `current_user` method.
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
- true
88
+ allow_read?(user)
58
89
  end
59
90
 
60
- # Subclasses can override this method to return the current user object. This object will
61
- # be passed to the authenticated?, allow_read?, allow_write?, and changed_by methods.
62
- # @param request [Rack::Request] current reqeust object
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 return the login URL for the application. If this is
90
- # provided, then a user will be redirected to that URL if they are not authenticated when loading
91
- # the HTML application.
92
- # @param request [Rack::Request] current reqeust object
93
- # @return [String]
94
- def login_url(request)
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
- @app.call(env)
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, {"Content-Type" => "text/html; charset=utf-8"}, [Application.new(:default, add_to_head(request)).render("index.html.erb")]]
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
- location = login_url(request)
131
- if location
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
- # `GET /`
10
+ # @example
11
+ # GET /
11
12
  #
12
- # The response payload is:
13
- # ```
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
- # ]
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
- # `GET /setting`
32
+ # @example
33
+ # GET /setting
34
34
  #
35
- # Query parameters
35
+ # Query parameters
36
36
  #
37
- # * key - setting key
37
+ # * key - setting key
38
38
  #
39
- # The response payload is:
40
- # ```
41
- # {
42
- # key: string,
43
- # value: object,
44
- # value_type: string,
45
- # description string,
46
- # created_at: iso8601 string,
47
- # updated_at: iso8601 string
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
- # `POST /settings`
54
+ # @example
55
+ # POST /settings
57
56
  #
58
- # The format of the parameters is an array of hashes with each setting identified by the key.
59
- # The settings should include either `value` and `value_type` (and optionally `description`) to
60
- # insert or update a setting, or `deleted` to delete the setting.
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
- # { settings: [
64
- # {
65
- # key: string,
66
- # value: object,
67
- # value_type: string,
68
- # description: string,
69
- # },
70
- # {
71
- # key: string,
72
- # deleted: boolean,
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
- # The response will be either `{success: true}` or `{success: false, errors: {key => [string], ...}}`
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
- # `GET /setting/history`
100
+ # @example
101
+ # GET /setting/history
98
102
  #
99
- # Query parameters
103
+ # Query parameters
100
104
  #
101
- # * key - setting key
102
- # * limit - number of history items to return
103
- # * offset - index to start fetching items from (most recent items are first)
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
- # The response format is:
106
- # ```
107
- # {
108
- # key: string,
109
- # encrypted: boolean,
110
- # histories: [
111
- # {
112
- # value: object,
113
- # changed_by: string,
114
- # created_at: iso8601 string
115
- # },
116
- # ...
117
- # ],
118
- # previous_page_params: hash,
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, encrypted: setting.encrypted?}
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
- # `GET /last_updated_at`
157
- # #
158
- # The response payload is:
159
- # {
160
- # last_updated_at: iso8601 string
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
- # `GET /updated_since`
170
+ # @example
171
+ # GET /updated_since
169
172
  #
170
- # Query parameters
173
+ # Query parameters
171
174
  #
172
- # * time - iso8601 string
175
+ # * time - iso8601 string
173
176
  #
174
- # The response payload is:
175
- # ```
176
- # [
177
- # {
178
- # key: string,
179
- # value: object,
180
- # value_type: string,
181
- # description string,
182
- # created_at: iso8601 string,
183
- # updated_at: iso8601 string
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)