super_settings 2.4.3 → 2.6.0
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/AGENTS.md +27 -0
- data/ARCHITECTURE.md +232 -0
- data/CHANGELOG.md +29 -0
- data/README.md +56 -6
- data/VERSION +1 -1
- data/app/helpers/super_settings/settings_helper.rb +5 -11
- data/app/locales/ar.json +44 -0
- data/app/locales/cs.json +44 -0
- data/app/locales/da.json +44 -0
- data/app/locales/de.json +44 -0
- data/app/locales/el.json +44 -0
- data/app/locales/en.json +55 -0
- data/app/locales/es.json +44 -0
- data/app/locales/fa.json +44 -0
- data/app/locales/fr.json +44 -0
- data/app/locales/gd.json +44 -0
- data/app/locales/he.json +44 -0
- data/app/locales/hi.json +44 -0
- data/app/locales/it.json +44 -0
- data/app/locales/ja.json +44 -0
- data/app/locales/ko.json +44 -0
- data/app/locales/lt.json +44 -0
- data/app/locales/nb.json +44 -0
- data/app/locales/nl.json +44 -0
- data/app/locales/pl.json +44 -0
- data/app/locales/pt-br.json +44 -0
- data/app/locales/pt.json +44 -0
- data/app/locales/ru.json +44 -0
- data/app/locales/sv.json +44 -0
- data/app/locales/ta.json +44 -0
- data/app/locales/tr.json +44 -0
- data/app/locales/uk.json +44 -0
- data/app/locales/ur.json +44 -0
- data/app/locales/vi.json +44 -0
- data/app/locales/zh-cn.json +44 -0
- data/app/locales/zh-tw.json +44 -0
- data/app/views/layouts/super_settings/settings.html.erb +4 -4
- data/config/routes.rb +2 -0
- data/lib/super_settings/application/api.js +37 -12
- data/lib/super_settings/application/helper.rb +78 -11
- data/lib/super_settings/application/index.html.erb +44 -44
- data/lib/super_settings/application/layout.html.erb +113 -6
- data/lib/super_settings/application/layout_styles.css +311 -21
- data/lib/super_settings/application/layout_vars.css.erb +15 -7
- data/lib/super_settings/application/scripts.js +72 -13
- data/lib/super_settings/application/style_vars.css.erb +108 -53
- data/lib/super_settings/application/styles.css +179 -84
- data/lib/super_settings/application.rb +21 -3
- data/lib/super_settings/configuration.rb +27 -5
- data/lib/super_settings/controller_actions.rb +39 -2
- data/lib/super_settings/http_client.rb +2 -1
- data/lib/super_settings/local_cache.rb +12 -16
- data/lib/super_settings/mini_i18n.rb +110 -0
- data/lib/super_settings/rack_application.rb +94 -7
- data/lib/super_settings/rest_api.rb +7 -3
- data/lib/super_settings/setting.rb +1 -1
- data/lib/super_settings/storage/active_record_storage/models.rb +7 -9
- data/lib/super_settings/storage/http_storage.rb +1 -2
- data/lib/super_settings/storage/json_storage.rb +1 -1
- data/lib/super_settings/storage/mongodb_storage.rb +2 -1
- data/lib/super_settings/storage/s3_storage.rb +1 -1
- data/lib/super_settings/storage.rb +1 -1
- data/lib/super_settings/version.rb +1 -1
- data/lib/super_settings.rb +16 -18
- data/super_settings.gemspec +2 -1
- metadata +35 -3
|
@@ -8,12 +8,16 @@ module SuperSettings
|
|
|
8
8
|
include Helper
|
|
9
9
|
|
|
10
10
|
# @param layout [String, Symbol] path to an ERB template to use as the layout around the application UI. You can
|
|
11
|
-
#
|
|
11
|
+
# pass the symbol +:default+ to use the default layout that ships with the gem.
|
|
12
12
|
# @param add_to_head [String] HTML code to add to the <head> element on the page.
|
|
13
13
|
# @param api_base_url [String] the base URL for the REST API.
|
|
14
14
|
# @param color_scheme [Symbol] whether to use dark mode for the application UI. If +nil+, the user's system
|
|
15
|
-
#
|
|
16
|
-
|
|
15
|
+
# preference will be used.
|
|
16
|
+
# @param dark_mode_selector [String] a CSS selector that sets dark mode when it matches an element in the page.
|
|
17
|
+
# This is an alternative to using the color_scheme option.
|
|
18
|
+
# @param read_only [Boolean] whether to render the application in read-only mode (edit controls hidden).
|
|
19
|
+
# @param locale [String] the locale code for translations (default: "en").
|
|
20
|
+
def initialize(layout: nil, add_to_head: nil, api_base_url: nil, color_scheme: nil, dark_mode_selector: nil, read_only: false, locale: nil)
|
|
17
21
|
if layout
|
|
18
22
|
layout = File.expand_path(File.join("application", "layout.html.erb"), __dir__) if layout == :default
|
|
19
23
|
@layout = ERB.new(File.read(layout)) if layout
|
|
@@ -25,6 +29,9 @@ module SuperSettings
|
|
|
25
29
|
|
|
26
30
|
@api_base_url = api_base_url
|
|
27
31
|
@color_scheme = color_scheme&.to_sym
|
|
32
|
+
@dark_mode_selector = dark_mode_selector
|
|
33
|
+
@read_only = !!read_only
|
|
34
|
+
@locale = locale || SuperSettings::MiniI18n::DEFAULT_LOCALE
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
# Render the web UI application HTML.
|
|
@@ -38,6 +45,17 @@ module SuperSettings
|
|
|
38
45
|
html
|
|
39
46
|
end
|
|
40
47
|
|
|
48
|
+
# Render the edit form HTML for a single setting.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] the rendered HTML
|
|
51
|
+
def render_edit
|
|
52
|
+
template = ERB.new(File.read(File.expand_path(File.join("application", "edit.html.erb"), __dir__)))
|
|
53
|
+
html = template.result(binding)
|
|
54
|
+
html = render_layout { html } if @layout
|
|
55
|
+
html = html.html_safe if html.respond_to?(:html_safe)
|
|
56
|
+
html
|
|
57
|
+
end
|
|
58
|
+
|
|
41
59
|
private
|
|
42
60
|
|
|
43
61
|
def render_layout
|
|
@@ -12,6 +12,8 @@ module SuperSettings
|
|
|
12
12
|
|
|
13
13
|
# Configuration for the controller.
|
|
14
14
|
class Controller
|
|
15
|
+
VALID_COLOR_SCHEMES = [:light, :dark, :system].freeze
|
|
16
|
+
|
|
15
17
|
# @api private
|
|
16
18
|
attr_reader :enhancement
|
|
17
19
|
|
|
@@ -25,12 +27,13 @@ module SuperSettings
|
|
|
25
27
|
def initialize
|
|
26
28
|
@superclass = nil
|
|
27
29
|
@web_ui_enabled = true
|
|
28
|
-
@color_scheme =
|
|
30
|
+
@color_scheme = nil
|
|
29
31
|
@changed_by_block = nil
|
|
30
32
|
@enhancement = nil
|
|
31
33
|
@application_name = nil
|
|
32
34
|
@application_logo = nil
|
|
33
35
|
@application_link = nil
|
|
36
|
+
@dark_mode_selector = nil
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def superclass
|
|
@@ -41,10 +44,10 @@ module SuperSettings
|
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
44
|
-
#
|
|
47
|
+
# Optional name of the application displayed in the view.
|
|
45
48
|
attr_accessor :application_name
|
|
46
49
|
|
|
47
|
-
# Optional
|
|
50
|
+
# Optional image URL for the application logo.
|
|
48
51
|
attr_accessor :application_logo
|
|
49
52
|
|
|
50
53
|
# Optional URL for a link back to the rest of the application.
|
|
@@ -69,11 +72,30 @@ module SuperSettings
|
|
|
69
72
|
end
|
|
70
73
|
|
|
71
74
|
# Set dark mode for the web UI. Possible values are :light, :dark, or :system.
|
|
72
|
-
#
|
|
75
|
+
# When the value is not set (or is invalid), theme selection is left dynamic.
|
|
73
76
|
attr_writer :color_scheme
|
|
74
77
|
|
|
78
|
+
# Return the configured color scheme.
|
|
79
|
+
#
|
|
80
|
+
# @return [Symbol, nil]
|
|
75
81
|
def color_scheme
|
|
76
|
-
|
|
82
|
+
scheme = @color_scheme&.to_sym
|
|
83
|
+
VALID_COLOR_SCHEMES.include?(scheme) ? scheme : nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Set a CSS selector that sets dark mode. This is an alternative to using the color_scheme
|
|
87
|
+
# option and can be used if you have a custom implementation for dark mode in your application.
|
|
88
|
+
# Dark mode will be enabled in the settings web UI when the selector matches an element in the page.
|
|
89
|
+
attr_accessor :dark_mode_selector
|
|
90
|
+
|
|
91
|
+
# Return the selector used to enable dark mode from host page state.
|
|
92
|
+
#
|
|
93
|
+
# @return [String, nil]
|
|
94
|
+
def resolved_dark_mode_selector
|
|
95
|
+
return @dark_mode_selector if @dark_mode_selector
|
|
96
|
+
return "[data-theme=dark]" if color_scheme.nil?
|
|
97
|
+
|
|
98
|
+
nil
|
|
77
99
|
end
|
|
78
100
|
|
|
79
101
|
# Enhance the controller. You can define methods or call controller class methods like
|
|
@@ -19,8 +19,13 @@ module SuperSettings
|
|
|
19
19
|
|
|
20
20
|
# Render the HTML application for managing settings.
|
|
21
21
|
def root
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
application = SuperSettings::Application.new(
|
|
23
|
+
read_only: super_settings_read_only?,
|
|
24
|
+
locale: I18n.locale,
|
|
25
|
+
color_scheme: SuperSettings.configuration.controller.color_scheme,
|
|
26
|
+
dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector
|
|
27
|
+
)
|
|
28
|
+
render html: application.render.html_safe, layout: true
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# API endpoint for getting active settings. See SuperSettings::RestAPI for details.
|
|
@@ -40,6 +45,10 @@ module SuperSettings
|
|
|
40
45
|
|
|
41
46
|
# API endpoint for updating settings. See SuperSettings::RestAPI for details.
|
|
42
47
|
def update
|
|
48
|
+
if super_settings_read_only?
|
|
49
|
+
render json: {error: "Access denied"}, status: 403
|
|
50
|
+
return
|
|
51
|
+
end
|
|
43
52
|
changed_by = SuperSettings.configuration.controller.changed_by(self)
|
|
44
53
|
result = SuperSettings::RestAPI.update(params[:settings], changed_by)
|
|
45
54
|
if result[:success]
|
|
@@ -69,12 +78,40 @@ module SuperSettings
|
|
|
69
78
|
render json: SuperSettings::RestAPI.updated_since(params[:time])
|
|
70
79
|
end
|
|
71
80
|
|
|
81
|
+
# API endpoint for checking if the user is authorized to edit settings.
|
|
82
|
+
def authorized
|
|
83
|
+
permission = super_settings_read_only? ? "read-only" : "read-write"
|
|
84
|
+
headers["super-settings-permission"] = permission
|
|
85
|
+
headers["cache-control"] = "no-cache"
|
|
86
|
+
render json: {authorized: true, permission: permission}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Serve up the api.js file that defines a JavaScript client for the REST API.
|
|
90
|
+
def api_js
|
|
91
|
+
js = File.read(File.expand_path(File.join("application", "api.js"), __dir__))
|
|
92
|
+
render js: js.html_safe, content_type: "application/javascript; charset=utf-8"
|
|
93
|
+
end
|
|
94
|
+
|
|
72
95
|
protected
|
|
73
96
|
|
|
97
|
+
# Mark the current request as read-only. When read-only, the web UI will hide edit
|
|
98
|
+
# controls and write API endpoints will return 403. Call this in a +before_action+ to
|
|
99
|
+
# restrict a user to read-only access.
|
|
100
|
+
def super_settings_read_only!
|
|
101
|
+
request.env["super_settings.read_only"] = true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Return true if the current request has been marked as read-only.
|
|
105
|
+
def super_settings_read_only?
|
|
106
|
+
!!request.env["super_settings.read_only"]
|
|
107
|
+
end
|
|
108
|
+
|
|
74
109
|
# Return true if CSRF protection needs to be enabled for the request.
|
|
75
110
|
# By default it is only enabled on stateful requests that include Basic authorization
|
|
76
111
|
# or cookies in the request so that stateless REST API calls are allowed.
|
|
77
112
|
def protect_from_forgery?
|
|
113
|
+
return false if action_name == "api_js"
|
|
114
|
+
|
|
78
115
|
request.cookies.present? || request.authorization.to_s.split(" ", 2).first&.match?(/\ABasic/i)
|
|
79
116
|
end
|
|
80
117
|
end
|
|
@@ -135,6 +135,7 @@ module SuperSettings
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def set_headers(request)
|
|
138
|
+
request.basic_auth(@user, @password) if @user
|
|
138
139
|
@headers.each do |name, value|
|
|
139
140
|
name = name.to_s
|
|
140
141
|
values = Array(value)
|
|
@@ -148,7 +149,7 @@ module SuperSettings
|
|
|
148
149
|
def request_uri(path, params = nil)
|
|
149
150
|
uri = URI.join(@base_uri, path.delete_prefix("/"))
|
|
150
151
|
if (params && !params.empty?) || (@base_uri.query && !@base_uri.query.empty?)
|
|
151
|
-
uri.query = [uri.query, query_string(params)].join("&")
|
|
152
|
+
uri.query = [uri.query, query_string(params)].compact.join("&")
|
|
152
153
|
end
|
|
153
154
|
uri
|
|
154
155
|
end
|
|
@@ -120,16 +120,14 @@ module SuperSettings
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
load_block = lambda do
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
values[setting.key] = setting.value.freeze
|
|
128
|
-
end
|
|
129
|
-
set_cache_values(start_time) { values }
|
|
130
|
-
ensure
|
|
131
|
-
@refreshing = false
|
|
123
|
+
values = {}
|
|
124
|
+
start_time = Time.now
|
|
125
|
+
Setting.active.each do |setting|
|
|
126
|
+
values[setting.key] = setting.value.freeze
|
|
132
127
|
end
|
|
128
|
+
set_cache_values(start_time) { values }
|
|
129
|
+
ensure
|
|
130
|
+
@refreshing = false
|
|
133
131
|
end
|
|
134
132
|
|
|
135
133
|
if asynchronous
|
|
@@ -158,14 +156,12 @@ module SuperSettings
|
|
|
158
156
|
end
|
|
159
157
|
|
|
160
158
|
refresh_block = lambda do
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
merge_load(last_refresh_time)
|
|
165
|
-
end
|
|
166
|
-
ensure
|
|
167
|
-
@refreshing = false
|
|
159
|
+
last_db_update = Setting.last_updated_at
|
|
160
|
+
if last_db_update.nil? || last_db_update >= last_refresh_time - 1
|
|
161
|
+
merge_load(last_refresh_time)
|
|
168
162
|
end
|
|
163
|
+
ensure
|
|
164
|
+
@refreshing = false
|
|
169
165
|
end
|
|
170
166
|
|
|
171
167
|
if asynchronous
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module SuperSettings
|
|
6
|
+
# Internationalization support for the web UI. Translations are stored as
|
|
7
|
+
# JSON files in +app/locales/+ with one file per locale (e.g. +en.json+).
|
|
8
|
+
#
|
|
9
|
+
# Ruby templates use `#t` to look up a dotted key. JavaScript receives the
|
|
10
|
+
# full translation hash inlined via a +<script>+ tag so that the same JSON
|
|
11
|
+
# file drives both server-side and client-side strings.
|
|
12
|
+
module MiniI18n
|
|
13
|
+
DEFAULT_LOCALE = "en"
|
|
14
|
+
|
|
15
|
+
@cache = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Return the list of available locale codes derived from the JSON files
|
|
20
|
+
# present in +app/locales/+.
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<String>] locale codes, e.g. +["en"]+
|
|
23
|
+
def available_locales
|
|
24
|
+
load_all_locales.keys.sort
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Look up a translation by dotted key for the given locale.
|
|
28
|
+
# Falls back to the default locale when the key is missing, and
|
|
29
|
+
# ultimately returns the key itself if no translation is found.
|
|
30
|
+
#
|
|
31
|
+
# @param key [String] dotted translation key, e.g. +"page.title"+
|
|
32
|
+
# @param locale [String] the locale code (default: +DEFAULT_LOCALE+)
|
|
33
|
+
# @return [String] the translated string
|
|
34
|
+
def t(key, locale: DEFAULT_LOCALE)
|
|
35
|
+
translations = translations_for(locale)
|
|
36
|
+
translations[key] || translations_for(DEFAULT_LOCALE)[key] || key
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return the full translation hash for a locale. Used to inline
|
|
40
|
+
# translations into the HTML page for JavaScript consumption.
|
|
41
|
+
#
|
|
42
|
+
# @param locale [String] the locale code
|
|
43
|
+
# @return [Hash<String, String>] all key/value translations
|
|
44
|
+
def translations_for(locale)
|
|
45
|
+
load_all_locales[normalize_locale(locale)] || load_all_locales[DEFAULT_LOCALE] || {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Return the text direction for the given locale. Reads the +"dir"+ key
|
|
49
|
+
# from the locale's translations hash, falling back to +"ltr"+ when not set.
|
|
50
|
+
#
|
|
51
|
+
# @param locale [String] the locale code
|
|
52
|
+
# @return [String] +"ltr"+ or +"rtl"+
|
|
53
|
+
def text_direction(locale = DEFAULT_LOCALE)
|
|
54
|
+
dir = translations_for(locale)["dir"]
|
|
55
|
+
(dir == "rtl") ? "rtl" : "ltr"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Clear the translation cache. Called automatically in development mode.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
def clear_cache!
|
|
62
|
+
@mutex.synchronize { @cache = {} }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Normalize a locale string to just the language subtag if the full
|
|
68
|
+
# tag is not available (e.g. "en-US" → "en").
|
|
69
|
+
def normalize_locale(locale)
|
|
70
|
+
locale = locale.to_s.strip.tr("_", "-").downcase
|
|
71
|
+
return locale if @cache.key?(locale)
|
|
72
|
+
|
|
73
|
+
# Try the language subtag (e.g. "en-us" → "en")
|
|
74
|
+
lang = locale.split("-").first
|
|
75
|
+
lang if @cache.key?(lang)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Load every JSON file from the locales directory, keyed by filename
|
|
79
|
+
# stem (e.g. "en").
|
|
80
|
+
def load_all_locales
|
|
81
|
+
if development_mode?
|
|
82
|
+
@mutex.synchronize { @cache = {} }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return @cache unless @cache.empty?
|
|
86
|
+
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
return @cache unless @cache.empty?
|
|
89
|
+
|
|
90
|
+
Dir.glob(File.join(locales_dir, "*.json")).each do |path|
|
|
91
|
+
code = File.basename(path, ".json").downcase
|
|
92
|
+
@cache[code] = JSON.parse(File.read(path))
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
# Skip malformed locale files
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@cache
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def locales_dir
|
|
102
|
+
File.expand_path(File.join("..", "..", "app", "locales"), __dir__)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def development_mode?
|
|
106
|
+
ENV.fetch("RACK_ENV", ENV.fetch("RAILS_ENV", "development")) == "development"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -104,9 +104,9 @@ module SuperSettings
|
|
|
104
104
|
# Subclasses can override this method to return the path to an ERB file to use as the layout
|
|
105
105
|
# for the HTML application. The layout can use any of the methods defined in SuperSettings::Application::Helper.
|
|
106
106
|
#
|
|
107
|
-
# @return [String]
|
|
107
|
+
# @return [String, Symbol]
|
|
108
108
|
def layout
|
|
109
|
-
|
|
109
|
+
:default
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
# Subclasses can override this method to add custom HTML to the <head> element in the HTML application.
|
|
@@ -133,12 +133,16 @@ module SuperSettings
|
|
|
133
133
|
if request.get?
|
|
134
134
|
if (path == "/" || path == "") && web_ui_enabled?
|
|
135
135
|
return handle_root_request(request)
|
|
136
|
+
elsif path == "/authorized"
|
|
137
|
+
return handle_authorization_request(request)
|
|
138
|
+
elsif path == "/api.js"
|
|
139
|
+
return handle_api_js_request(request)
|
|
136
140
|
elsif path == "/settings"
|
|
137
141
|
return handle_index_request(request)
|
|
138
|
-
elsif path == "/setting"
|
|
139
|
-
return handle_show_request(request)
|
|
140
142
|
elsif path == "/setting/history"
|
|
141
143
|
return handle_history_request(request)
|
|
144
|
+
elsif path == "/setting"
|
|
145
|
+
return handle_show_request(request)
|
|
142
146
|
elsif path == "/last_updated_at"
|
|
143
147
|
return handle_last_updated_at_request(request)
|
|
144
148
|
elsif path == "/updated_since"
|
|
@@ -157,9 +161,40 @@ module SuperSettings
|
|
|
157
161
|
end
|
|
158
162
|
end
|
|
159
163
|
|
|
164
|
+
def handle_authorization_request(request)
|
|
165
|
+
check_authorization(request) do |user|
|
|
166
|
+
read_only = !allow_write?(user) || !!request.env["super_settings.read_only"]
|
|
167
|
+
permission = read_only ? "read-only" : "read-write"
|
|
168
|
+
payload = {authorized: true, permission: permission}
|
|
169
|
+
[200, {"content-type" => "application/json; charset=utf-8", "cache-control" => "no-cache", "super-settings-permission" => permission}, [JSON.generate(payload)]]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def handle_api_js_request(request)
|
|
174
|
+
check_authorization(request) do |user|
|
|
175
|
+
js = File.read(File.expand_path(File.join("application", "api.js"), __dir__))
|
|
176
|
+
[200, {"content-type" => "application/javascript; charset=utf-8", "cache-control" => "no-cache"}, [js]]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
160
180
|
def handle_root_request(request)
|
|
161
|
-
response = check_authorization(request
|
|
162
|
-
|
|
181
|
+
response = check_authorization(request) do |user|
|
|
182
|
+
read_only = !allow_write?(user) || !!request.env["super_settings.read_only"]
|
|
183
|
+
locale = resolve_locale(request)
|
|
184
|
+
headers = {"content-type" => "text/html; charset=utf-8", "cache-control" => "no-cache"}
|
|
185
|
+
lang = request.GET["lang"] if request.respond_to?(:GET)
|
|
186
|
+
if lang && SuperSettings::MiniI18n.available_locales.include?(lang)
|
|
187
|
+
headers["set-cookie"] = "super_settings_locale=#{lang}; path=/; SameSite=Lax"
|
|
188
|
+
end
|
|
189
|
+
application = Application.new(
|
|
190
|
+
layout: layout,
|
|
191
|
+
add_to_head: add_to_head(request),
|
|
192
|
+
color_scheme: SuperSettings.configuration.controller.color_scheme,
|
|
193
|
+
dark_mode_selector: SuperSettings.configuration.controller.resolved_dark_mode_selector,
|
|
194
|
+
read_only: read_only,
|
|
195
|
+
locale: locale
|
|
196
|
+
)
|
|
197
|
+
[200, headers, [application.render]]
|
|
163
198
|
end
|
|
164
199
|
|
|
165
200
|
if [401, 403].include?(response.first)
|
|
@@ -224,11 +259,15 @@ module SuperSettings
|
|
|
224
259
|
|
|
225
260
|
def check_authorization(request, write_required: false)
|
|
226
261
|
user = current_user(request)
|
|
227
|
-
return json_response(401, error: "
|
|
262
|
+
return json_response(401, error: "Authentication required") unless authenticated?(user)
|
|
228
263
|
|
|
229
264
|
allowed = (write_required ? allow_write?(user) : allow_read?(user))
|
|
230
265
|
return json_response(403, error: "Access denied") unless allowed
|
|
231
266
|
|
|
267
|
+
if write_required && request.env["super_settings.read_only"]
|
|
268
|
+
return json_response(403, error: "Access denied")
|
|
269
|
+
end
|
|
270
|
+
|
|
232
271
|
yield(user)
|
|
233
272
|
end
|
|
234
273
|
|
|
@@ -236,6 +275,54 @@ module SuperSettings
|
|
|
236
275
|
[status, RESPONSE_HEADERS.dup, [payload.to_json]]
|
|
237
276
|
end
|
|
238
277
|
|
|
278
|
+
# Determine the locale for a request. Precedence:
|
|
279
|
+
# 1. ?lang= query parameter
|
|
280
|
+
# 2. super_settings_locale cookie (set by the language picker)
|
|
281
|
+
# 3. Accept-Language header
|
|
282
|
+
# 4. Default locale
|
|
283
|
+
def resolve_locale(request)
|
|
284
|
+
available = SuperSettings::MiniI18n.available_locales
|
|
285
|
+
|
|
286
|
+
# 1. Explicit query parameter
|
|
287
|
+
lang = request.GET["lang"] if request.respond_to?(:GET)
|
|
288
|
+
return lang if lang && available.include?(lang)
|
|
289
|
+
|
|
290
|
+
# 2. Cookie
|
|
291
|
+
cookie = request.cookies["super_settings_locale"] if request.respond_to?(:cookies)
|
|
292
|
+
return cookie if cookie && available.include?(cookie)
|
|
293
|
+
|
|
294
|
+
# 3. Accept-Language header
|
|
295
|
+
accept = request.env["HTTP_ACCEPT_LANGUAGE"] if request.respond_to?(:env)
|
|
296
|
+
locale_from_accept_language(accept.to_s, available) || SuperSettings::MiniI18n::DEFAULT_LOCALE
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Parse the Accept-Language header and return the best matching locale.
|
|
300
|
+
def locale_from_accept_language(header, available)
|
|
301
|
+
return nil if header.nil? || header.empty?
|
|
302
|
+
|
|
303
|
+
# Parse tags with optional quality values, e.g. "en-US,en;q=0.9,fr;q=0.8"
|
|
304
|
+
tags = header.split(",").map { |entry|
|
|
305
|
+
parts = entry.strip.split(";")
|
|
306
|
+
tag = parts[0].to_s.strip.downcase.tr("_", "-")
|
|
307
|
+
q = 1.0
|
|
308
|
+
parts[1..].each do |p|
|
|
309
|
+
if p.strip.start_with?("q=")
|
|
310
|
+
q = p.strip.sub("q=", "").to_f
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
[tag, q]
|
|
314
|
+
}.sort_by { |_, q| -q }
|
|
315
|
+
|
|
316
|
+
tags.each do |tag, _|
|
|
317
|
+
return tag if available.include?(tag)
|
|
318
|
+
# Try language subtag
|
|
319
|
+
lang = tag.split("-").first
|
|
320
|
+
return lang if available.include?(lang)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
nil
|
|
324
|
+
end
|
|
325
|
+
|
|
239
326
|
def post_params(request)
|
|
240
327
|
if request.content_type.to_s.match?(/\Aapplication\/json/i) && request.body
|
|
241
328
|
request.params.merge(JSON.parse(request.body.read))
|
|
@@ -83,7 +83,7 @@ module SuperSettings
|
|
|
83
83
|
#
|
|
84
84
|
# The response will be either
|
|
85
85
|
#
|
|
86
|
-
# {success: true}
|
|
86
|
+
# {success: true, values: {key => value, ...}}
|
|
87
87
|
#
|
|
88
88
|
# or
|
|
89
89
|
#
|
|
@@ -93,7 +93,11 @@ module SuperSettings
|
|
|
93
93
|
def update(settings_params, changed_by = nil)
|
|
94
94
|
all_valid, settings = Setting.bulk_update(Array(settings_params), changed_by)
|
|
95
95
|
if all_valid
|
|
96
|
-
{
|
|
96
|
+
values = {}
|
|
97
|
+
settings.each do |setting|
|
|
98
|
+
values[setting.key] = setting.value
|
|
99
|
+
end
|
|
100
|
+
{success: true, values: values}
|
|
97
101
|
else
|
|
98
102
|
errors = {}
|
|
99
103
|
settings.each do |setting|
|
|
@@ -179,7 +183,7 @@ module SuperSettings
|
|
|
179
183
|
#
|
|
180
184
|
# @return [Hash] hash with the last updated timestamp
|
|
181
185
|
def last_updated_at
|
|
182
|
-
{last_updated_at: Setting.last_updated_at
|
|
186
|
+
{last_updated_at: Setting.last_updated_at&.utc&.iso8601(6)}
|
|
183
187
|
end
|
|
184
188
|
|
|
185
189
|
# Return settings that have been updated since a specified timestamp.
|
|
@@ -266,7 +266,7 @@ module SuperSettings
|
|
|
266
266
|
key = setting_params["key"]
|
|
267
267
|
setting = changed[key] || Setting.find_by_key(key)
|
|
268
268
|
unless setting
|
|
269
|
-
next if Coerce.present?(setting_params["
|
|
269
|
+
next if Coerce.present?(setting_params["deleted"])
|
|
270
270
|
|
|
271
271
|
setting = Setting.new(key: setting_params["key"])
|
|
272
272
|
end
|
|
@@ -17,15 +17,13 @@ module SuperSettings
|
|
|
17
17
|
# ActiveRecord storage is only available if the connection pool is connected and the table exists.
|
|
18
18
|
|
|
19
19
|
def available?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
false
|
|
28
|
-
end
|
|
20
|
+
# table_exists? will attempt to retrieve a connection from the pool and load the schema_cache
|
|
21
|
+
# which is memoized per connection. If there is no database or connection, it will raise an error.
|
|
22
|
+
table_exists?
|
|
23
|
+
rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
|
|
24
|
+
# Ignore errors so the application doesn't break if the database is not available.
|
|
25
|
+
# Otherwise things like build processes can fail.
|
|
26
|
+
false
|
|
29
27
|
end
|
|
30
28
|
end
|
|
31
29
|
end
|
|
@@ -6,7 +6,7 @@ module SuperSettings
|
|
|
6
6
|
# This storage engine is read only. It is intended to allow microservices to read settings from a
|
|
7
7
|
# central application that exposes the SuperSettings::RestAPI.
|
|
8
8
|
#
|
|
9
|
-
# You must
|
|
9
|
+
# You must set the base_url class attribute to the base URL of a SuperSettings REST API endpoint.
|
|
10
10
|
# You can also set the timeout, headers, and query_params used in reqeusts to the API.
|
|
11
11
|
class HttpStorage < StorageAttributes
|
|
12
12
|
include Storage
|
|
@@ -135,7 +135,6 @@ module SuperSettings
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def reload
|
|
138
|
-
self.class.find_by_key(key)
|
|
139
138
|
self.attributes = self.class.find_by_key(key).attributes
|
|
140
139
|
self
|
|
141
140
|
end
|
|
@@ -112,7 +112,7 @@ module SuperSettings
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
#
|
|
115
|
+
# Helper method to load settings from a JSON string.
|
|
116
116
|
#
|
|
117
117
|
# @param json [String] JSON string to parse.
|
|
118
118
|
# @return [Array<SuperSettings::Storage::JSONStorage>] Array of settings.
|
|
@@ -119,6 +119,7 @@ module SuperSettings
|
|
|
119
119
|
|
|
120
120
|
def save_all(changes)
|
|
121
121
|
upserts = changes.collect { |setting| upsert(setting) }
|
|
122
|
+
return true if upserts.empty?
|
|
122
123
|
settings_collection.bulk_write(upserts)
|
|
123
124
|
true
|
|
124
125
|
end
|
|
@@ -191,7 +192,7 @@ module SuperSettings
|
|
|
191
192
|
pipeline << {
|
|
192
193
|
"$addFields": {
|
|
193
194
|
history: {
|
|
194
|
-
"$slice": ["$history", offset,
|
|
195
|
+
"$slice": ["$history", offset, limit || {"$size": "$history"}]
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
}
|
|
@@ -17,7 +17,7 @@ module SuperSettings
|
|
|
17
17
|
|
|
18
18
|
def self.included(base)
|
|
19
19
|
base.extend(ClassMethods)
|
|
20
|
-
base.include(Attributes) unless base.
|
|
20
|
+
base.include(Attributes) unless base.method_defined?(:attributes=)
|
|
21
21
|
|
|
22
22
|
base.instance_variable_set(:@load_asynchronous, nil)
|
|
23
23
|
end
|