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.
@@ -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)