Module: KairosMcp::Admin::Helpers

Included in:
Router
Defined in:
lib/kairos_mcp/admin/helpers.rb

Overview

Helpers: ERB template helpers for the admin UI

Provides HTML escaping, template rendering, flash messages, CSRF protection, and session management utilities.

Constant Summary collapse

VIEWS_DIR =
File.expand_path('views', __dir__)
STATIC_DIR =
File.expand_path('static', __dir__)
'kairos_admin_session'
SESSION_SECRET =
ENV['KAIROS_SESSION_SECRET'] || SecureRandom.hex(32)

Instance Method Summary collapse

Instance Method Details

Build Set-Cookie header to clear session

Returns:

  • (String)

    Set-Cookie header value



163
164
165
# File 'lib/kairos_mcp/admin/helpers.rb', line 163

def clear_session_cookie
  "#{SESSION_COOKIE}=; Path=/admin; HttpOnly; SameSite=Strict; Max-Age=0"
end

#decode_session(cookie_value) ⇒ Hash?

Decode and verify a signed session cookie

Parameters:

  • cookie_value (String)

    Raw cookie value

Returns:

  • (Hash, nil)

    Session data or nil if invalid



128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/kairos_mcp/admin/helpers.rb', line 128

def decode_session(cookie_value)
  return nil unless cookie_value

  encoded, signature = cookie_value.split('--', 2)
  return nil unless encoded && signature
  return nil unless secure_compare(sign(encoded), signature)

  payload = encoded.unpack1('m0')
  JSON.parse(payload, symbolize_names: true)
rescue StandardError
  nil
end

#encode_session(data) ⇒ String

Encode a session value into a signed cookie

Parameters:

  • data (Hash)

    Session data

Returns:

  • (String)

    Signed cookie value



117
118
119
120
121
122
# File 'lib/kairos_mcp/admin/helpers.rb', line 117

def encode_session(data)
  payload = JSON.generate(data)
  encoded = [payload].pack('m0') # Base64 (no newlines)
  signature = sign(encoded)
  "#{encoded}--#{signature}"
end

#generate_csrf_tokenString

Generate a CSRF token

Returns:

  • (String)

    CSRF token



174
175
176
# File 'lib/kairos_mcp/admin/helpers.rb', line 174

def generate_csrf_token
  SecureRandom.hex(32)
end

#get_session(env) ⇒ Hash?

Extract session from Rack env

Parameters:

  • env (Hash)

    Rack environment

Returns:

  • (Hash, nil)

    Session data



145
146
147
148
149
# File 'lib/kairos_mcp/admin/helpers.rb', line 145

def get_session(env)
  cookies = parse_cookies(env)
  cookie_value = cookies[SESSION_COOKIE]
  decode_session(cookie_value)
end

#h(text) ⇒ String

HTML-escape a string to prevent XSS

Parameters:

  • text (String, nil)

    Raw text

Returns:

  • (String)

    HTML-escaped text



22
23
24
# File 'lib/kairos_mcp/admin/helpers.rb', line 22

def h(text)
  ERB::Util.html_escape(text.to_s)
end

#html_response(status, body) ⇒ Array

Generate an HTML response

Parameters:

  • status (Integer)

    HTTP status code

  • body (String)

    HTML body

Returns:

  • (Array)

    Rack response triple



74
75
76
# File 'lib/kairos_mcp/admin/helpers.rb', line 74

def html_response(status, body)
  [status, { 'Content-Type' => 'text/html; charset=utf-8' }, [body]]
end

#parse_cookies(env) ⇒ Hash

Parse cookies from Rack env

Parameters:

  • env (Hash)

    Rack environment

Returns:

  • (Hash)

    Cookie name → value



205
206
207
208
209
210
211
# File 'lib/kairos_mcp/admin/helpers.rb', line 205

def parse_cookies(env)
  cookie_header = env['HTTP_COOKIE'] || ''
  cookie_header.split(';').each_with_object({}) do |pair, hash|
    key, value = pair.strip.split('=', 2)
    hash[key] = value if key
  end
end

#parse_form_body(body) ⇒ Hash

Parse URL-encoded form body

Parameters:

  • body (String)

    Form body

Returns:

  • (Hash)

    Parameter name → value



217
218
219
220
221
222
# File 'lib/kairos_mcp/admin/helpers.rb', line 217

def parse_form_body(body)
  body.split('&').each_with_object({}) do |pair, hash|
    key, value = pair.split('=', 2)
    hash[URI.decode_www_form_component(key)] = URI.decode_www_form_component(value || '')
  end
end

#parse_query(env) ⇒ Hash

Parse query string

Parameters:

  • env (Hash)

    Rack environment

Returns:

  • (Hash)

    Query parameters



228
229
230
231
# File 'lib/kairos_mcp/admin/helpers.rb', line 228

def parse_query(env)
  qs = env['QUERY_STRING'] || ''
  parse_form_body(qs)
end

#redirect(path) ⇒ Array

Redirect response

Parameters:

  • path (String)

    Redirect target

Returns:

  • (Array)

    Rack response triple



82
83
84
# File 'lib/kairos_mcp/admin/helpers.rb', line 82

def redirect(path)
  [302, { 'Location' => path }, []]
end

#render(template_name, layout: true, **locals) ⇒ String

Render an ERB template

Parameters:

  • template_name (String)

    Template file name (without .erb)

  • layout (Boolean) (defaults to: true)

    Whether to wrap in layout

  • locals (Hash)

    Local variables for the template

Returns:

  • (String)

    Rendered HTML



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/kairos_mcp/admin/helpers.rb', line 32

def render(template_name, layout: true, **locals)
  template_path = File.join(VIEWS_DIR, "#{template_name}.erb")
  template = ERB.new(File.read(template_path), trim_mode: '-')

  # Make locals available as instance variables for ERB binding
  b = binding
  locals.each { |k, v| b.local_variable_set(k, v) }

  content = template.result(b)

  if layout
    render_layout(content)
  else
    content
  end
end

#render_layout(content) ⇒ String

Wrap content in the shared layout

Parameters:

  • content (String)

    Page content HTML

Returns:

  • (String)

    Full HTML page



62
63
64
65
66
67
# File 'lib/kairos_mcp/admin/helpers.rb', line 62

def render_layout(content)
  @content = content
  layout_path = File.join(VIEWS_DIR, 'layout.erb')
  layout_template = ERB.new(File.read(layout_path), trim_mode: '-')
  layout_template.result(binding)
end

#render_partial(partial_name, **locals) ⇒ String

Render a partial (no layout)

Parameters:

  • partial_name (String)

    Partial file name (without .erb, with _prefix)

  • locals (Hash)

    Local variables

Returns:

  • (String)

    Rendered HTML fragment



54
55
56
# File 'lib/kairos_mcp/admin/helpers.rb', line 54

def render_partial(partial_name, **locals)
  render("partials/#{partial_name}", layout: false, **locals)
end

#serve_static(filename) ⇒ Array

Serve a static file

Parameters:

  • filename (String)

    File name in static/ directory

Returns:

  • (Array)

    Rack response triple



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/kairos_mcp/admin/helpers.rb', line 90

def serve_static(filename)
  filepath = File.join(STATIC_DIR, filename)
  return [404, {}, ['Not found']] unless File.exist?(filepath)

  content_type = case File.extname(filename)
                 when '.css' then 'text/css'
                 when '.js'  then 'application/javascript'
                 when '.png' then 'image/png'
                 when '.svg' then 'image/svg+xml'
                 else 'application/octet-stream'
                 end

  [200, { 'Content-Type' => content_type, 'Cache-Control' => 'public, max-age=3600' },
   [File.read(filepath)]]
end

Build Set-Cookie header for session

Parameters:

  • data (Hash)

    Session data

Returns:

  • (String)

    Set-Cookie header value



155
156
157
158
# File 'lib/kairos_mcp/admin/helpers.rb', line 155

def session_cookie(data)
  value = encode_session(data)
  "#{SESSION_COOKIE}=#{value}; Path=/admin; HttpOnly; SameSite=Strict"
end

#valid_csrf?(env, session) ⇒ Boolean

Verify a CSRF token from form submission

Parameters:

  • env (Hash)

    Rack environment

  • session (Hash)

    Current session

Returns:

  • (Boolean)

    Whether CSRF token is valid



183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/kairos_mcp/admin/helpers.rb', line 183

def valid_csrf?(env, session)
  body = env['rack.input']&.read
  env['rack.input']&.rewind
  return false unless body

  params = parse_form_body(body)
   = params['_csrf']
  session_token = session&.dig(:csrf_token)

  return false unless  && session_token

  secure_compare(, session_token)
end