Class: SolidObjects::Web

Inherits:
Object
  • Object
show all
Defined in:
lib/solid_objects/web.rb,
lib/solid_objects/web/route.rb,
lib/solid_objects/web/action.rb,
lib/solid_objects/web/router.rb,
lib/solid_objects/web/helpers.rb,
lib/solid_objects/web/paginator.rb,
lib/solid_objects/web/statistics.rb,
lib/solid_objects/web/application.rb,
lib/solid_objects/web/csrf_protection.rb,
sig/generated/lib/solid_objects/web.rbs,
sig/generated/lib/solid_objects/web/route.rbs,
sig/generated/lib/solid_objects/web/action.rbs,
sig/generated/lib/solid_objects/web/router.rbs,
sig/generated/lib/solid_objects/web/helpers.rbs,
sig/generated/lib/solid_objects/web/paginator.rbs,
sig/generated/lib/solid_objects/web/statistics.rbs,
sig/generated/lib/solid_objects/web/application.rbs,
sig/generated/lib/solid_objects/web/csrf_protection.rbs

Overview

An operator dashboard for the actor runtime, served as a Rack application:

Rails.application.routes.draw do
mount SolidObjects::Web => "/solid_objects"
end

It is deliberately not loaded by require "solid_objects". A worker process must not carry a web stack, and an application that never mounts the dashboard must not pay for it.

Every page asks configuration.authorize_administration first. That block denies by default, so a mount alone exposes nothing until an application states who may read it.

Defined Under Namespace

Modules: Helpers, Router Classes: Action, Application, CsrfProtection, Paginator, Route, Statistics

Constant Summary collapse

ROOT =

Returns:

  • (Object)
File.expand_path("../../web", __dir__)
VIEWS =

Returns:

  • (Object)
File.join(ROOT, "views")
ASSETS =

Returns:

  • (Object)
File.join(ROOT, "assets")
NONCE_KEY =

Returns:

  • (::String)
"solid_objects.content_security_policy_nonce"
CSRF_TOKEN_KEY =

Returns:

  • (::String)
"solid_objects.csrf_token"
NONCE_BYTES =

Returns:

  • (::Integer)
16
ASSET_CACHE_SECONDS =

Returns:

  • (::Integer)
86_400
TEMPLATE_NAME =

Returns:

  • (::Regexp)
/\A_?[a-z][a-z0-9_]*\z/
CHART_LIBRARY_URL =

The dashboard charts need a charting library, and this one is fetched from a public CDN with a subresource integrity hash, so a compromised CDN cannot substitute other code. A deployment with no outbound network access should vendor the file and point chart_library_url at it, or set that to nil to render the dashboard without charts.

Returns:

  • (::String)
"https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"
CHART_LIBRARY_INTEGRITY =

Returns:

  • (::String)
"sha384-XcdcwHqIPULERb2yDEM4R0XaQKU3YnDsrTmjACBZyfdVVqjh6xQ4/DCMd7XLcA6Y"
DEFAULT_TABS =

Returns:

  • (Object)
{
  "Dashboard" => "/",
  "Instances" => "/instances",
  "Mailbox" => "/mailbox",
  "Reminders" => "/reminders",
  "Effects" => "/effects",
  "Broadcasts" => "/broadcasts",
  "Dead letters" => "/dead_letters",
  "Processes" => "/processes"
}.freeze
LOCK =

Returns:

  • (Object)
Mutex.new

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.chart_library_integrityString?

RBS:

  • () -> String?

Returns:

  • (String, nil)


81
82
83
# File 'lib/solid_objects/web.rb', line 81

def chart_library_integrity
  defined?(@chart_library_integrity) ? @chart_library_integrity : CHART_LIBRARY_INTEGRITY
end

.chart_library_urlString?

RBS:

  • () -> String?

Returns:

  • (String, nil)


76
77
78
# File 'lib/solid_objects/web.rb', line 76

def chart_library_url
  defined?(@chart_library_url) ? @chart_library_url : CHART_LIBRARY_URL
end

Instance Attribute Details

#chart_library_integrity=(value) ⇒ Object (writeonly)

A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy. nil renders the dashboard without charts.

RBS:

  • @chart_library_url: String?

  • @chart_library_integrity: String?

Parameters:

  • value (Object)


59
60
61
# File 'sig/generated/lib/solid_objects/web.rbs', line 59

def chart_library_integrity=(value)
  @chart_library_integrity = value
end

#chart_library_url=(value) ⇒ Object (writeonly)

A path below the mount serves a vendored copy; an absolute URL is fetched from that host and is named in the content security policy. nil renders the dashboard without charts.

RBS:

  • @chart_library_url: String?

  • @chart_library_integrity: String?

Parameters:

  • value (Object)


52
53
54
# File 'sig/generated/lib/solid_objects/web.rbs', line 52

def chart_library_url=(value)
  @chart_library_url = value
end

Class Method Details

.applicationWeb

RBS:

  • () -> Web

Returns:



170
171
172
# File 'lib/solid_objects/web.rb', line 170

def application
  LOCK.synchronize { @application ||= new }
end

.call(env) ⇒ Array[untyped]

RBS:

  • (Hash[String, untyped]) -> Array[untyped]

Parameters:

  • (Hash[String, untyped])

Returns:

  • (Array[untyped])


165
166
167
# File 'lib/solid_objects/web.rb', line 165

def call(env)
  application.call(env)
end

.chart_library_originString?

The origin the content security policy has to allow. A vendored copy served from the mount has none, so the policy stays at 'self'.

RBS:

  • () -> String?

Returns:

  • (String, nil)


93
94
95
96
97
98
99
100
101
102
103
# File 'lib/solid_objects/web.rb', line 93

def chart_library_origin
  url = chart_library_url
  return nil unless url&.include?("//")

  uri = URI.parse(url)
  return nil unless uri.scheme && uri.host

  "#{uri.scheme}://#{uri.host}"
rescue URI::InvalidURIError
  nil
end

.charts?Boolean

RBS:

  • () -> bool

Returns:

  • (Boolean)


86
87
88
# File 'lib/solid_objects/web.rb', line 86

def charts?
  !chart_library_url.nil?
end

.middlewaresArray[[ Array[untyped], Proc? ]]

RBS:

  • () -> Array[[Array[untyped], Proc?]]

Returns:

  • (Array[[ Array[untyped], Proc? ]])


118
119
120
# File 'lib/solid_objects/web.rb', line 118

def middlewares
  @middlewares ||= []
end

.register(extension, tab:, path:, views: nil) ⇒ void

This method returns an undefined value.

Adds pages to the dashboard. The extension receives the application class and declares its own routes on it, which means its routes carry an authorization policy like every other route.

RBS:

  • (untyped, tab: String, path: String, ?views: String?) -> void

Parameters:

  • (Object)
  • tab: (String)
  • path: (String)
  • views: (String, nil) (defaults to: nil)


135
136
137
138
139
140
141
142
143
144
# File 'lib/solid_objects/web.rb', line 135

def register(extension, tab:, path:, views: nil)
  if views
    self.views.unshift(views)
    # A template compiled before this call resolved against the old
    # search path, so a replacement view would never be reached.
    LOCK.synchronize { @templates = {} }
  end
  tabs[tab] = path
  extension.registered(Application)
end

.reset!void

This method returns an undefined value.

RBS:

  • () -> void



154
155
156
157
158
159
160
161
162
# File 'lib/solid_objects/web.rb', line 154

def reset!
  LOCK.synchronize { @templates = {} }
  @tabs = nil
  @views = nil
  @middlewares = nil
  @application = nil
  remove_instance_variable(:@chart_library_url) if defined?(@chart_library_url)
  remove_instance_variable(:@chart_library_integrity) if defined?(@chart_library_integrity)
end

.tabsHash[String, String]

RBS:

  • () -> Hash[String, String]

Returns:

  • (Hash[String, String])


106
107
108
# File 'lib/solid_objects/web.rb', line 106

def tabs
  @tabs ||= DEFAULT_TABS.dup
end

.template(name) ⇒ Object

RBS:

  • (Symbol) -> untyped

Parameters:

  • (Symbol)

Returns:

  • (Object)


147
148
149
150
151
# File 'lib/solid_objects/web.rb', line 147

def template(name)
  LOCK.synchronize do
    templates[name] ||= ERB.new(File.read(template_path(name)), trim_mode: "-")
  end
end

.use(*arguments, &block) ⇒ void

This method returns an undefined value.

The built stack is memoized, so a middleware added after the first request would otherwise be dropped without a word.

RBS:

  • (*untyped) ?{ () -> untyped } -> void

Parameters:

  • (Object)


125
126
127
128
# File 'lib/solid_objects/web.rb', line 125

def use(*arguments, &block)
  middlewares << [ arguments, block ]
  LOCK.synchronize { @application = nil }
end

.viewsArray[String]

Searched in order, so an extension directory added first wins over the packaged one and an application can replace a single page.

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


113
114
115
# File 'lib/solid_objects/web.rb', line 113

def views
  @views ||= [ VIEWS ]
end

Instance Method Details

#appObject

RBS:

  • () -> untyped

Returns:

  • (Object)


199
200
201
# File 'lib/solid_objects/web.rb', line 199

def app
  @app ||= build
end

#buildObject

RBS:

  • () -> untyped

Returns:

  • (Object)


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/solid_objects/web.rb', line 206

def build
  assets = ASSETS
  extra = self.class.middlewares

  ::Rack::Builder.new do
    use ::Rack::Static,
      urls: [ "/stylesheets", "/javascripts" ],
      root: assets,
      cascade: true,
      header_rules: [ [ :all, { "cache-control" => "private, max-age=#{ASSET_CACHE_SECONDS}" } ] ]
    extra.each { |arguments, block| use(*arguments, &block) }
    use CsrfProtection
    run Application.new
  end
end

#call(env) ⇒ Array[untyped]

RBS:

  • (Hash[String, untyped]) -> Array[untyped]

Parameters:

  • (Hash[String, untyped])

Returns:

  • (Array[untyped])


193
194
195
196
# File 'lib/solid_objects/web.rb', line 193

def call(env)
  env[NONCE_KEY] = SecureRandom.base64(NONCE_BYTES)
  app.call(env)
end