solid_queue_ui 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +42 -0
  4. data/Rakefile +3 -0
  5. data/lib/solid_queue_ui/config.rb +26 -0
  6. data/lib/solid_queue_ui/rails.rb +11 -0
  7. data/lib/solid_queue_ui/railtie.rb +4 -0
  8. data/lib/solid_queue_ui/version.rb +3 -0
  9. data/lib/solid_queue_ui/web/action.rb +93 -0
  10. data/lib/solid_queue_ui/web/application.rb +126 -0
  11. data/lib/solid_queue_ui/web/csrf_protection.rb +180 -0
  12. data/lib/solid_queue_ui/web/database.rb +17 -0
  13. data/lib/solid_queue_ui/web/helpers.rb +201 -0
  14. data/lib/solid_queue_ui/web/router.rb +102 -0
  15. data/lib/solid_queue_ui/web.rb +145 -0
  16. data/lib/solid_queue_ui.rb +22 -0
  17. data/lib/tasks/solid_queue_ui_tasks.rake +4 -0
  18. data/solid_queue_ui.gemspec +42 -0
  19. data/web/assets/javascripts/application.js +177 -0
  20. data/web/assets/javascripts/chart.min.js +13 -0
  21. data/web/assets/stylesheets/application.css +37 -0
  22. data/web/assets/stylesheets/application.css.scss +15 -0
  23. data/web/views/_footer.html.erb +1 -0
  24. data/web/views/_nav.html.erb +11 -0
  25. data/web/views/dashboard.html.erb +33 -0
  26. data/web/views/jobs.html.erb +41 -0
  27. data/web/views/layout.html.erb +32 -0
  28. data/web/views/solid_queue_ui/application/_flashes.html.erb +8 -0
  29. data/web/views/solid_queue_ui/application/_index_header.html.erb +28 -0
  30. data/web/views/solid_queue_ui/application/_javascript.html.erb +13 -0
  31. data/web/views/solid_queue_ui/application/_navigation.html.erb +11 -0
  32. data/web/views/solid_queue_ui/application/_stylesheet.html.erb +5 -0
  33. data/web/views/solid_queue_ui/application/index.html.erb +21 -0
  34. data/web/views/solid_queue_ui/application/show.html.erb +57 -0
  35. metadata +127 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 985a9c19e4975054736df9f365ef44c9860a40e63a170519f98261aa29c14f33
4
+ data.tar.gz: 45626fc20b57ca1ffd2a10611714d17be6dc22406e7eb402e3ae9e10d5594fcf
5
+ SHA512:
6
+ metadata.gz: 0fca4095ab55709ac773f525d177b98cd0be3a1c8153ec380fe03c7b9e75fe10374bb1c64de2e5afcc3cf9fa207526390b6c538dfbadd9eeeb7f3e52de9747ba
7
+ data.tar.gz: 9005fd05e2630acec335360b804a7ca5b0f69c576f398178ebdccab485a3cc8f16654ffa46a9c4af35a9f9b0574baac34fb2a4bd5842d0747e40ca5ba5ebef69
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Claude Ayitey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # SolidQueueUi
2
+ I wanted to setup a quick UI to see jobs run by solid_queue. It's currently very barebones.
3
+
4
+ ## Usage
5
+ This gem heavily mimics the implementation of the Sidekiq UI. To view the jobs, here's what to do:
6
+
7
+ Update `config/routes.rb` to mount SolidQueueUI::Web like so:
8
+
9
+ ```
10
+ Rails.application.routes.draw do
11
+ mount SolidQueueUi::Web => '/solid_queue_ui'
12
+
13
+ # all your routes.
14
+ end
15
+ ```
16
+
17
+ The main page should be now visible at `your_url/solid_queue_ui` as specified. You can use any string after the slash and it will work accordingly.
18
+
19
+ This is the first version. Wanted to release this and make improvements. There's no polling, turbo refreshes, other tables, editing, sorting, NO TESTS, etc. Just wanted to put this out first.
20
+
21
+ ## Installation
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem "solid_queue_ui"
26
+ ```
27
+
28
+ And then execute:
29
+ ```bash
30
+ $ bundle
31
+ ```
32
+
33
+ Or install it yourself as:
34
+ ```bash
35
+ $ gem install solid_queue_ui
36
+ ```
37
+
38
+ ## Contributing
39
+ You can just fork and submit a PR for your issue.
40
+
41
+ ## License
42
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ require "set"
2
+
3
+ module SolidQueueUi
4
+ class Config
5
+
6
+ DEFAULTS = {
7
+ labels: Set.new,
8
+ require: ".",
9
+ }
10
+
11
+ def initialize(options = {})
12
+ @directory = {}
13
+ end
14
+
15
+ def to_json(*)
16
+ SolidQueueUi.dump_json(@options)
17
+ end
18
+
19
+
20
+ private def parameter_size(handler)
21
+ target = handler.is_a?(Proc) ? handler : handler.method(:call)
22
+ target.parameters.size
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ require "rails"
2
+
3
+ module SolidQueueUi
4
+ class Rails < ::Rails::Engine
5
+ initializer "solid_queue_ui.active_job_integration" do
6
+ ActiveSupport.on_load(:active_job) do
7
+ include ::SolidQueueUi::Job::Options unless respond_to?(:solid_queue_ui_options)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module SolidQueueUi
2
+ class Railtie < ::Rails::Railtie
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module SolidQueueUi
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueUi
4
+ class WebAction
5
+ RACK_SESSION = "rack.session"
6
+
7
+ attr_accessor :env, :block, :type
8
+
9
+ def settings
10
+ Web.settings
11
+ end
12
+
13
+ def request
14
+ @request ||= ::Rack::Request.new(env)
15
+ end
16
+
17
+ def halt(res)
18
+ throw :halt, [res, {Rack::CONTENT_TYPE => "text/plain"}, [res.to_s]]
19
+ end
20
+
21
+ def redirect(location)
22
+ throw :halt, [302, {Web::LOCATION => "#{request.base_url}#{location}"}, []]
23
+ end
24
+
25
+ def params
26
+ indifferent_hash = Hash.new { |hash, key| hash[key.to_s] if Symbol === key }
27
+
28
+ indifferent_hash.merge! request.params
29
+ route_params.each { |k, v| indifferent_hash[k.to_s] = v }
30
+
31
+ indifferent_hash
32
+ end
33
+
34
+ def route_params
35
+ env[WebRouter::ROUTE_PARAMS]
36
+ end
37
+
38
+ def session
39
+ env[RACK_SESSION]
40
+ end
41
+
42
+ def erb(content, options = {})
43
+ if content.is_a? Symbol
44
+ unless respond_to?(:"_erb_#{content}")
45
+ src = ERB.new(File.read("#{Web.settings.views}/#{content}.html.erb")).src
46
+ WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
47
+ def _erb_#{content}
48
+ #{src}
49
+ end
50
+ RUBY
51
+ end
52
+ end
53
+
54
+ if @_erb
55
+ _erb(content, options[:locals])
56
+ else
57
+ @_erb = true
58
+ content = _erb(content, options[:locals])
59
+
60
+ _render { content }
61
+ end
62
+ end
63
+
64
+ def render(engine, content, options = {})
65
+ raise "Only erb templates are supported" if engine != :erb
66
+
67
+ erb(content, options)
68
+ end
69
+
70
+ def json(payload)
71
+ [200, {Rack::CONTENT_TYPE => "application/json", Rack::CACHE_CONTROL => "private, no-store"}, [SolidQueueUi.dump_json(payload)]]
72
+ end
73
+
74
+ def initialize(env, block)
75
+ @_erb = false
76
+ @env = env
77
+ @block = block
78
+ @files ||= {}
79
+ end
80
+
81
+ private
82
+
83
+ def _erb(file, locals)
84
+ locals&.each { |k, v| define_singleton_method(k) { v } unless singleton_methods.include? k }
85
+
86
+ if file.is_a?(String)
87
+ ERB.new(file).result(binding)
88
+ else
89
+ send(:"_erb_#{file}")
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueUi
4
+ class WebApplication
5
+ extend WebRouter
6
+
7
+ CSP_HEADER = [
8
+ "default-src 'self' https: http:",
9
+ "child-src 'self'",
10
+ "connect-src 'self' https: http: wss: ws:",
11
+ "font-src 'self' https: http:",
12
+ "frame-src 'self'",
13
+ "img-src 'self' https: http: data:",
14
+ "manifest-src 'self'",
15
+ "media-src 'self'",
16
+ "object-src 'none'",
17
+ "script-src 'self' https: http:",
18
+ "style-src 'self' https: http: 'unsafe-inline'",
19
+ "worker-src 'self'",
20
+ "base-uri 'self'"
21
+ ].join("; ").freeze
22
+
23
+ METRICS_PERIODS = {
24
+ "1h" => 60,
25
+ "2h" => 120,
26
+ "4h" => 240,
27
+ "8h" => 480
28
+ }
29
+
30
+ def initialize(klass)
31
+ @klass = klass
32
+ end
33
+
34
+ def settings
35
+ @klass.settings
36
+ end
37
+
38
+ def self.settings
39
+ SolidQueueUi::Web.settings
40
+ end
41
+
42
+ def self.tabs
43
+ SolidQueueUi::Web.tabs
44
+ end
45
+
46
+ def self.set(key, val)
47
+ # nothing, backwards compatibility
48
+ end
49
+
50
+ get "/" do
51
+ @sq_jobs = ActiveRecord::Base.connection.execute("SELECT * FROM solid_queue_jobs ORDER BY priority ASC")
52
+ erb(:dashboard)
53
+ end
54
+
55
+ # get "/jobs" do
56
+ # @sq_jobs = ActiveRecord::Base.connection.execute("SELECT * FROM solid_queue_jobs ORDER BY priority ASC")
57
+ # erb(:jobs)
58
+ # end
59
+
60
+ def call(env)
61
+ action = self.class.match(env)
62
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
63
+
64
+ app = @klass
65
+ resp = catch(:halt) do
66
+ self.class.run_befores(app, action)
67
+ action.instance_exec env, &action.block
68
+ ensure
69
+ self.class.run_afters(app, action)
70
+ end
71
+
72
+ case resp
73
+ when Array
74
+ # redirects go here
75
+ resp
76
+ else
77
+ # rendered content goes here
78
+ headers = {
79
+ Rack::CONTENT_TYPE => "text/html",
80
+ Rack::CACHE_CONTROL => "private, no-store",
81
+ Web::CONTENT_LANGUAGE => action.locale,
82
+ Web::CONTENT_SECURITY_POLICY => CSP_HEADER
83
+ }
84
+ # we'll let Rack calculate Content-Length for us.
85
+ [200, headers, [resp]]
86
+ end
87
+ end
88
+
89
+ def self.helpers(mod = nil, &block)
90
+ if block
91
+ WebAction.class_eval(&block)
92
+ else
93
+ WebAction.send(:include, mod)
94
+ end
95
+ end
96
+
97
+ def self.before(path = nil, &block)
98
+ befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
99
+ end
100
+
101
+ def self.after(path = nil, &block)
102
+ afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
103
+ end
104
+
105
+ def self.run_befores(app, action)
106
+ run_hooks(befores, app, action)
107
+ end
108
+
109
+ def self.run_afters(app, action)
110
+ run_hooks(afters, app, action)
111
+ end
112
+
113
+ def self.run_hooks(hooks, app, action)
114
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
115
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
116
+ end
117
+
118
+ def self.befores
119
+ @befores ||= []
120
+ end
121
+
122
+ def self.afters
123
+ @afters ||= []
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file originally based on authenticity_token.rb from the sinatra/rack-protection project
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2011-2017 Konstantin Haase
8
+ # Copyright (c) 2015-2017 Zachary Scott
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # 'Software'), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require "securerandom"
30
+ require "base64"
31
+ require "rack/request"
32
+
33
+ module SolidQueueUi
34
+ class Web
35
+ class CsrfProtection
36
+ def initialize(app, options = nil)
37
+ @app = app
38
+ end
39
+
40
+ def call(env)
41
+ accept?(env) ? admit(env) : deny(env)
42
+ end
43
+
44
+ private
45
+
46
+ def admit(env)
47
+ # On each successful request, we create a fresh masked token
48
+ # which will be used in any forms rendered for this request.
49
+ s = session(env)
50
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
51
+ env[:csrf_token] = mask_token(s[:csrf])
52
+ @app.call(env)
53
+ end
54
+
55
+ def safe?(env)
56
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
57
+ end
58
+
59
+ def logger(env)
60
+ @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
61
+ end
62
+
63
+ def deny(env)
64
+ logger(env).warn "attack prevented by #{self.class}"
65
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
66
+ end
67
+
68
+ def session(env)
69
+ env["rack.session"] || fail(<<~EOM)
70
+ SolidQueueUi::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
71
+ make sure you mount SolidQueueUi::Web *inside* your application routes:
72
+
73
+
74
+ Rails.application.routes.draw do
75
+ mount SolidQueueUi::Web => "/solid_queue_ui"
76
+ ....
77
+ end
78
+
79
+
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
83
+
84
+ If this is a bare Rack app, use a session middleware before SolidQueueUi::Web:
85
+
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
88
+
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run SolidQueueUi::Web
92
+
93
+ EOM
94
+ end
95
+
96
+ def accept?(env)
97
+ return true if safe?(env)
98
+
99
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
100
+ valid_token?(env, giventoken)
101
+ end
102
+
103
+ TOKEN_LENGTH = 32
104
+
105
+ # Checks that the token given to us as a parameter matches
106
+ # the token stored in the session.
107
+ def valid_token?(env, giventoken)
108
+ return false if giventoken.nil? || giventoken.empty?
109
+
110
+ begin
111
+ token = decode_token(giventoken)
112
+ rescue ArgumentError # client input is invalid
113
+ return false
114
+ end
115
+
116
+ sess = session(env)
117
+ localtoken = sess[:csrf]
118
+
119
+ # Checks that Rack::Session::Cookie actualy contains the csrf toekn
120
+ return false if localtoken.nil?
121
+
122
+ # Rotate the session token after every use
123
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
124
+
125
+ # See if it's actually a masked token or not. We should be able
126
+ # to handle any unmasked tokens that we've issued without error.
127
+
128
+ if unmasked_token?(token)
129
+ compare_with_real_token token, localtoken
130
+ elsif masked_token?(token)
131
+ unmasked = unmask_token(token)
132
+ compare_with_real_token unmasked, localtoken
133
+ else
134
+ false # Token is malformed
135
+ end
136
+ end
137
+
138
+ # Creates a masked version of the authenticity token that varies
139
+ # on each request. The masking is used to mitigate SSL attacks
140
+ # like BREACH.
141
+ def mask_token(token)
142
+ token = decode_token(token)
143
+ one_time_pad = SecureRandom.random_bytes(token.length)
144
+ encrypted_token = xor_byte_strings(one_time_pad, token)
145
+ masked_token = one_time_pad + encrypted_token
146
+ Base64.urlsafe_encode64(masked_token)
147
+ end
148
+
149
+ # Essentially the inverse of +mask_token+.
150
+ def unmask_token(masked_token)
151
+ # Split the token into the one-time pad and the encrypted
152
+ # value and decrypt it
153
+ token_length = masked_token.length / 2
154
+ one_time_pad = masked_token[0...token_length]
155
+ encrypted_token = masked_token[token_length..]
156
+ xor_byte_strings(one_time_pad, encrypted_token)
157
+ end
158
+
159
+ def unmasked_token?(token)
160
+ token.length == TOKEN_LENGTH
161
+ end
162
+
163
+ def masked_token?(token)
164
+ token.length == TOKEN_LENGTH * 2
165
+ end
166
+
167
+ def compare_with_real_token(token, local)
168
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
+ end
170
+
171
+ def decode_token(token)
172
+ Base64.urlsafe_decode64(token)
173
+ end
174
+
175
+ def xor_byte_strings(s1, s2)
176
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # require "yaml"
3
+
4
+ # require "rails"
5
+ # require "solid_queue_ui/rails"
6
+ # require "erb"
7
+
8
+ # module SolidQueueUi
9
+ # class Database
10
+
11
+ # def self.db_config
12
+ # # Rails.application.config.database_configuration[Rails.env] ||= YAML.load(ERB.new(File.read('./config/database.yml')).result)
13
+ # Rails.application.config.database_configuration[Rails.env]
14
+ # # YAML.load(ERB.new(File.read('./config/database.yml')).result)
15
+ # end
16
+ # end
17
+ # end