kiosk-server 0.6.1 → 0.6.28
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/CHANGELOG.md +138 -0
- data/README.md +134 -102
- data/app/channels/kiosk_events.rb +30 -17
- data/app/controllers/kiosk/server/application_controller.rb +0 -1
- data/app/controllers/kiosk/server/assistants_controller.rb +13 -14
- data/app/controllers/kiosk/server/auth_controller.rb +3 -3
- data/app/controllers/kiosk/server/device_verify_controller.rb +0 -5
- data/app/controllers/kiosk/server/discovery_controller.rb +2 -7
- data/app/controllers/kiosk/server/kyc_callback_controller.rb +3 -1
- data/app/controllers/kiosk/server/oauth_device_authorization_controller.rb +8 -7
- data/app/controllers/kiosk/server/oauth_token_controller.rb +4 -1
- data/app/controllers/kiosk/server/payment_setup_controller.rb +3 -4
- data/app/controllers/kiosk/server/wire_controller.rb +2 -2
- data/app/models/kiosk/checkout_mandate.rb +0 -2
- data/app/models/kiosk/settlement.rb +0 -2
- data/app/models/kiosk/visit.rb +0 -2
- data/app/views/kiosk/server/assistants/show.html.erb +20 -24
- data/app/views/kiosk/server/device_verify/decided.html.erb +1 -0
- data/app/views/kiosk/server/device_verify/show.html.erb +1 -0
- data/app/views/layouts/kiosk/server/application.html.erb +14 -0
- data/lib/generators/kiosk/install/install_generator.rb +25 -64
- data/lib/generators/kiosk/install/templates/create_kiosk_device_authorizations.rb.tt +21 -12
- data/lib/generators/kiosk/install/templates/create_kiosk_events.rb.tt +15 -17
- data/lib/generators/kiosk/install/templates/create_kiosk_identity_tables.rb.tt +39 -10
- data/lib/generators/kiosk/install/templates/create_kiosk_kyc_attributes.rb.tt +16 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_mandates.rb.tt +50 -14
- data/lib/generators/kiosk/install/templates/create_kiosk_pow_spent.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_refunds.rb.tt +11 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_revocations.rb.tt +7 -11
- data/lib/generators/kiosk/install/templates/create_kiosk_visits.rb.tt +10 -11
- data/lib/generators/kiosk/install/templates/initializer.rb.tt +13 -4
- data/lib/kiosk/payable.rb +1 -1
- data/lib/kiosk/server/account_binding.rb +14 -5
- data/lib/kiosk/server/account_holder_gate.rb +3 -1
- data/lib/kiosk/server/agent_identity_providers/default_agent_idp.rb +1 -1
- data/lib/kiosk/server/agent_rotation.rb +1 -1
- data/lib/kiosk/server/argument_decoder.rb +2 -2
- data/lib/kiosk/server/assistant_accounts.rb +38 -0
- data/lib/kiosk/server/auth_toll.rb +5 -5
- data/lib/kiosk/server/client.rb +1 -1
- data/lib/kiosk/server/client_assertion.rb +3 -2
- data/lib/kiosk/server/client_registration.rb +6 -2
- data/lib/kiosk/server/column_spending_cap.rb +1 -1
- data/lib/kiosk/server/configuration_extension.rb +15 -38
- data/lib/kiosk/server/current_request.rb +1 -1
- data/lib/kiosk/server/device_authorization.rb +1 -1
- data/lib/kiosk/server/device_authorization_stores.rb +1 -1
- data/lib/kiosk/server/device_code_grant.rb +2 -2
- data/lib/kiosk/server/dpop.rb +6 -1
- data/lib/kiosk/server/engine.rb +2 -2
- data/lib/kiosk/server/errors.rb +21 -7
- data/lib/kiosk/server/event_stores.rb +2 -2
- data/lib/kiosk/server/events.rb +4 -3
- data/lib/kiosk/server/events_connection.rb +23 -5
- data/lib/kiosk/server/events_replay.rb +79 -0
- data/lib/kiosk/server/executor/repeat.rb +1 -1
- data/lib/kiosk/server/executor/spending_cap.rb +8 -66
- data/lib/kiosk/server/executor.rb +10 -10
- data/lib/kiosk/server/failure_log.rb +3 -3
- data/lib/kiosk/server/handler_mixin.rb +9 -9
- data/lib/kiosk/server/handler_registrations.rb +1 -1
- data/lib/kiosk/server/handler_serving.rb +11 -11
- data/lib/kiosk/server/headers.rb +3 -3
- data/lib/kiosk/server/identity_resolution.rb +1 -1
- data/lib/kiosk/server/kyc.rb +5 -7
- data/lib/kiosk/server/kyc_verifier.rb +26 -27
- data/lib/kiosk/server/linked_assistants.rb +21 -16
- data/lib/kiosk/server/money.rb +41 -0
- data/lib/kiosk/server/oauth_answers.rb +1 -0
- data/lib/kiosk/server/oauth_error.rb +3 -0
- data/lib/kiosk/server/open_api.rb +9 -9
- data/lib/kiosk/server/page.rb +1 -1
- data/lib/kiosk/server/payment_claim.rb +3 -3
- data/lib/kiosk/server/pow_gate.rb +12 -33
- data/lib/kiosk/server/pow_spent_stores.rb +1 -1
- data/lib/kiosk/server/receipts.rb +4 -3
- data/lib/kiosk/server/reputation_factors.rb +12 -5
- data/lib/kiosk/server/request_validation.rb +2 -1
- data/lib/kiosk/server/result.rb +1 -1
- data/lib/kiosk/server/revocation_stores.rb +1 -1
- data/lib/kiosk/server/routing.rb +1 -1
- data/lib/kiosk/server/schemas/auth.schema.json +2 -2
- data/lib/kiosk/server/schemas/binding.schema.json +3 -3
- data/lib/kiosk/server/schemas/kyc.schema.json +22 -3
- data/lib/kiosk/server/schemas/mandates.schema.json +5 -5
- data/lib/kiosk/server/schemas/pow.schema.json +2 -2
- data/lib/kiosk/server/session_context.rb +2 -2
- data/lib/kiosk/server/tolled_calls.rb +46 -0
- data/lib/kiosk/server/verb_wire.rb +3 -3
- data/lib/kiosk/server/version.rb +1 -1
- data/lib/kiosk/server/well_known.rb +14 -11
- data/lib/kiosk/server/wire_rendering.rb +1 -1
- data/lib/kiosk/server.rb +6 -2
- metadata +9 -8
- data/lib/generators/kiosk/install/templates/create_kiosk_schema.rb.tt +0 -17
- data/lib/kiosk/server/schema_definitions.rb +0 -320
- data/lib/kiosk/server/settled_spend.rb +0 -21
- data/lib/kiosk/server/wrong_door.rb +0 -42
|
@@ -8,11 +8,8 @@ module Kiosk
|
|
|
8
8
|
class AssistantsController < ApplicationController
|
|
9
9
|
SIGN_IN_PROMPT = 'Sign in to your account first to manage linked assistants.'
|
|
10
10
|
SIGN_IN_ALERT = 'Please sign in to manage your linked assistants.'
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
'it needs a signed-in session and a CSRF token from its own form'
|
|
14
|
-
|
|
15
|
-
layout false
|
|
11
|
+
CAP_NOT_AN_AMOUNT = 'Write the spending cap as an amount, such as 50 or 12.50.'
|
|
12
|
+
CURRENCY_NOT_A_CODE = 'Write the cap currency as a three-letter currency code, such as EUR.'
|
|
16
13
|
|
|
17
14
|
def show
|
|
18
15
|
return unless require_account_holder!(prompt: SIGN_IN_PROMPT, flash_alert: SIGN_IN_ALERT)
|
|
@@ -41,7 +38,7 @@ module Kiosk
|
|
|
41
38
|
render_error(e)
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
# Edits a bound assistant's label and/or spending cap and its currency (blank cap →
|
|
41
|
+
# Edits a bound assistant's label and/or spending cap and its currency (blank cap → no cap).
|
|
45
42
|
def update
|
|
46
43
|
return unless require_account_holder!(prompt: SIGN_IN_PROMPT, flash_alert: SIGN_IN_ALERT)
|
|
47
44
|
|
|
@@ -62,8 +59,9 @@ module Kiosk
|
|
|
62
59
|
|
|
63
60
|
def render_page(status: :ok)
|
|
64
61
|
@assistants = LinkedAssistants.list(@identity.user_id)
|
|
65
|
-
#
|
|
66
|
-
@
|
|
62
|
+
# The spend shows where the origin takes payments; the cap, where a `spending_cap` reads it.
|
|
63
|
+
@payments = !Kiosk.configuration.payment_provider.nil?
|
|
64
|
+
@spending_cap = !Kiosk.configuration.spending_cap.nil?
|
|
67
65
|
@page_path = request.path.sub(%r{/(link|unlink|update)\z}, '')
|
|
68
66
|
render :show, status: status
|
|
69
67
|
end
|
|
@@ -72,24 +70,25 @@ module Kiosk
|
|
|
72
70
|
def settings_changes
|
|
73
71
|
changes = {}
|
|
74
72
|
changes['human_label'] = params[:human_label].to_s if params.key?(:human_label)
|
|
75
|
-
changes['spending_cap_cents'], changes['spending_cap_currency'] = spending_cap_param if params.key?(:
|
|
73
|
+
changes['spending_cap_cents'], changes['spending_cap_currency'] = spending_cap_param if params.key?(:spending_cap)
|
|
76
74
|
changes
|
|
77
75
|
end
|
|
78
76
|
|
|
79
|
-
# `[cents, currency]
|
|
77
|
+
# `[cents, currency]` from the amount the person typed, or `[nil, nil]` for no cap.
|
|
80
78
|
def spending_cap_param
|
|
81
|
-
raw = params[:
|
|
79
|
+
raw = params[:spending_cap].to_s
|
|
82
80
|
return [nil, nil] if raw.strip.empty?
|
|
83
81
|
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
currency = spending_cap_currency_param
|
|
83
|
+
cents = Money.parse(raw, currency) || raise(Errors::BadRequest, CAP_NOT_AN_AMOUNT)
|
|
84
|
+
[cents, currency]
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
def spending_cap_currency_param
|
|
89
88
|
code = MandateVerifier.canonical_currency(params[:spending_cap_currency])
|
|
90
89
|
return code if MandateVerifier::CURRENCY_CODE.match?(code)
|
|
91
90
|
|
|
92
|
-
raise Errors::BadRequest,
|
|
91
|
+
raise Errors::BadRequest, CURRENCY_NOT_A_CODE
|
|
93
92
|
end
|
|
94
93
|
end
|
|
95
94
|
end
|
|
@@ -17,7 +17,7 @@ module Kiosk
|
|
|
17
17
|
render json: payload, status: status
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
# The same RFC 9457 problem document as the
|
|
20
|
+
# The same RFC 9457 problem document as the protocol (§9).
|
|
21
21
|
def render_error(err)
|
|
22
22
|
add_problem_headers(err)
|
|
23
23
|
render json: err.to_problem, status: err.http_status,
|
|
@@ -32,7 +32,7 @@ module Kiosk
|
|
|
32
32
|
response.set_header('WWW-Authenticate', challenge) if challenge
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# RFC 7235 challenge header for the 402s, as
|
|
35
|
+
# RFC 7235 challenge header for the 402s, as in the protocol.
|
|
36
36
|
def www_authenticate_for(err)
|
|
37
37
|
issuer = Kiosk.current_issuer
|
|
38
38
|
case err
|
|
@@ -119,7 +119,7 @@ module Kiosk
|
|
|
119
119
|
def authenticated_account_holder!
|
|
120
120
|
identity = Kiosk.configuration.user_idp&.verify(request)
|
|
121
121
|
hint = "sign in to the provider first — this endpoint authenticates via the provider's own session"
|
|
122
|
-
raise Errors::
|
|
122
|
+
raise Errors::SessionRequired.new('account session required', hint:) unless identity
|
|
123
123
|
raise Errors::BadRequest, 'Content-Type must be application/json' unless request.media_type == 'application/json'
|
|
124
124
|
|
|
125
125
|
identity
|
|
@@ -11,14 +11,9 @@ module Kiosk
|
|
|
11
11
|
|
|
12
12
|
CODE_NOT_RECOGNISED = 'That code was not recognised — it may have expired. Ask the assistant for a fresh one.'
|
|
13
13
|
|
|
14
|
-
WRONG_DOOR_MESSAGE = "this is the account holder's browser consent page, not the Kiosk wire — " \
|
|
15
|
-
'it needs a signed-in session and a CSRF token from its own form'
|
|
16
|
-
|
|
17
14
|
# Failed code lookups per session before a 429, against 31^8 possible codes.
|
|
18
15
|
MAX_CODE_ATTEMPTS = 10
|
|
19
16
|
|
|
20
|
-
layout false
|
|
21
|
-
|
|
22
17
|
def show
|
|
23
18
|
return unless require_account_holder!(prompt: SIGN_IN_PROMPT, flash_alert: SIGN_IN_ALERT)
|
|
24
19
|
return render_attempts_capped if attempts_capped?
|
|
@@ -10,15 +10,14 @@ module Kiosk
|
|
|
10
10
|
# Rails stamps `Vary: Accept` on a negotiated render; these documents are
|
|
11
11
|
# one answer for everybody, so a shared cache must keep one copy.
|
|
12
12
|
after_action :drop_vary
|
|
13
|
+
before_action :allow_cors
|
|
13
14
|
|
|
14
15
|
def agents_txt
|
|
15
|
-
allow_cors
|
|
16
16
|
render plain: WellKnown.agents_txt(base_url: request.base_url),
|
|
17
17
|
content_type: 'text/plain; charset=utf-8'
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def agents_json
|
|
21
|
-
allow_cors
|
|
22
21
|
short_ttl
|
|
23
22
|
render json: WellKnown.agents_json(base_url: request.base_url)
|
|
24
23
|
end
|
|
@@ -28,17 +27,14 @@ module Kiosk
|
|
|
28
27
|
end
|
|
29
28
|
|
|
30
29
|
def oauth_protected_resource
|
|
31
|
-
allow_cors
|
|
32
30
|
render json: WellKnown.oauth_protected_resource(base_url: request.base_url)
|
|
33
31
|
end
|
|
34
32
|
|
|
35
33
|
def agent_skills_index
|
|
36
|
-
allow_cors
|
|
37
34
|
render_or_not_found WellKnown.agent_skills_index
|
|
38
35
|
end
|
|
39
36
|
|
|
40
37
|
def ard
|
|
41
|
-
allow_cors
|
|
42
38
|
short_ttl
|
|
43
39
|
render json: WellKnown.ard(base_url: request.base_url)
|
|
44
40
|
end
|
|
@@ -49,7 +45,6 @@ module Kiosk
|
|
|
49
45
|
end
|
|
50
46
|
|
|
51
47
|
def api_catalog
|
|
52
|
-
allow_cors
|
|
53
48
|
short_ttl
|
|
54
49
|
# RFC 9727 §2: a HEAD request finds the catalog by this header.
|
|
55
50
|
response.set_header('Link', %(<#{request.base_url}/.well-known/api-catalog>; rel="api-catalog"))
|
|
@@ -59,7 +54,6 @@ module Kiosk
|
|
|
59
54
|
end
|
|
60
55
|
|
|
61
56
|
def auth_md
|
|
62
|
-
allow_cors
|
|
63
57
|
render plain: WellKnown.auth_md(base_url: request.base_url),
|
|
64
58
|
content_type: 'text/markdown; charset=utf-8'
|
|
65
59
|
end
|
|
@@ -71,6 +65,7 @@ module Kiosk
|
|
|
71
65
|
end
|
|
72
66
|
|
|
73
67
|
# agents.txt v1.0 and AI Catalog require `Access-Control-Allow-Origin: *`.
|
|
68
|
+
# Every document here answers it, so a page on any origin can read it.
|
|
74
69
|
def allow_cors
|
|
75
70
|
response.set_header('Access-Control-Allow-Origin', '*')
|
|
76
71
|
end
|
|
@@ -7,7 +7,9 @@ module Kiosk
|
|
|
7
7
|
class KycCallbackController < WireController
|
|
8
8
|
def create
|
|
9
9
|
Kyc.served!
|
|
10
|
-
|
|
10
|
+
body = parse_body!
|
|
11
|
+
RequestValidation.validate_body!(body, exchange: 'POST <endpoint>/kyc/callback')
|
|
12
|
+
Kyc.callback(body)
|
|
11
13
|
render json: { ok: true }, status: :ok
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -24,15 +24,16 @@ module Kiosk
|
|
|
24
24
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
# assistant does not choose it.
|
|
27
|
+
# A `role` is refused: the role is the approving human's, and the
|
|
28
|
+
# assistant does not choose it. A `scope` is ignored (RFC 6749 Section
|
|
29
|
+
# 3.3): an assistant gets another role only when a human who holds that
|
|
30
|
+
# role binds it to their account, or through the identity provider.
|
|
29
31
|
def role_problem
|
|
30
|
-
|
|
31
|
-
return unless offending
|
|
32
|
+
return unless params.key?(:role)
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
34
|
+
'role is not accepted here: an assistant does not choose its own role. ' \
|
|
35
|
+
'The role of a bound assistant is the role of the account holder who approves, ' \
|
|
36
|
+
"and this provider's identity system gives it at the verify page."
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def device_authorization(result)
|
|
@@ -48,7 +48,10 @@ module Kiosk
|
|
|
48
48
|
client
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# RFC 6749 Section 5.1. The scope is the role of the client.
|
|
51
|
+
# RFC 6749 Section 5.1. The scope is the role of the client. A requested
|
|
52
|
+
# `scope` is ignored (RFC 6749 Section 3.3): an assistant gets another
|
|
53
|
+
# role only when a human who holds that role binds it to their account,
|
|
54
|
+
# or through the identity provider.
|
|
52
55
|
def render_token(client)
|
|
53
56
|
token = AgentIdentityProviders::DefaultAgentIdp.new.issue(agent_id: client.id, role: client.role)
|
|
54
57
|
body = { access_token: token, token_type: Dpop::SCHEME, expires_in: Kiosk.configuration.access_token_ttl }
|
|
@@ -18,12 +18,11 @@ module Kiosk
|
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
+
# No script and no inline style: a host Content-Security-Policy of `default-src 'self'` holds.
|
|
21
22
|
def page(title, text)
|
|
22
23
|
<<~HTML
|
|
23
|
-
<!DOCTYPE html><html><head><meta charset="utf-8"><title>#{title}</title></head>
|
|
24
|
-
<body
|
|
25
|
-
<h1>#{title}</h1><p>#{text}</p>
|
|
26
|
-
</body></html>
|
|
24
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>#{title}</title></head>
|
|
25
|
+
<body><main><h1>#{title}</h1><p>#{text}</p></main></body></html>
|
|
27
26
|
HTML
|
|
28
27
|
end
|
|
29
28
|
end
|
|
@@ -6,8 +6,8 @@ require 'action_dispatch/http/parameters'
|
|
|
6
6
|
module Kiosk
|
|
7
7
|
module Server
|
|
8
8
|
# The reserved `GET <endpoint>/schema` (public) and `POST <endpoint>/pay`,
|
|
9
|
-
# and the base every
|
|
10
|
-
#
|
|
9
|
+
# and the base every WireController inherits its seams from.
|
|
10
|
+
# Response (JSON): success is the handler's payload VERBATIM; an error
|
|
11
11
|
# is an RFC 9457 problem document.
|
|
12
12
|
class WireController < ::ActionController::API
|
|
13
13
|
include VerbWire
|
|
@@ -5,8 +5,6 @@ module Kiosk
|
|
|
5
5
|
# items of the operator's checkout that the assistant agreed to pay for.
|
|
6
6
|
# Read-only for the operator.
|
|
7
7
|
class CheckoutMandate < Server::ApplicationRecord
|
|
8
|
-
def self.table_name = "#{Kiosk.configuration.schema}.checkout_mandates"
|
|
9
|
-
|
|
10
8
|
has_many :settlements, class_name: 'Kiosk::Settlement', dependent: nil, inverse_of: :checkout_mandate
|
|
11
9
|
|
|
12
10
|
# Checkout mandates with a line item carrying these attributes, by jsonb
|
data/app/models/kiosk/visit.rb
CHANGED
|
@@ -5,8 +5,6 @@ module Kiosk
|
|
|
5
5
|
# value counts toward the customer's recent spend, so an operator that takes
|
|
6
6
|
# no payment can still lower the proof-of-work toll for a returning customer.
|
|
7
7
|
class Visit < Server::ApplicationRecord
|
|
8
|
-
def self.table_name = "#{Kiosk.configuration.schema}.visits"
|
|
9
|
-
|
|
10
8
|
# Call it when the visit happens, with what the visit was worth.
|
|
11
9
|
def self.record!(user_id:, value_cents:) = create!(user_id:, value_cents:)
|
|
12
10
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<%# «Link an assistant» page (link flow — Kiosk extension).
|
|
2
2
|
Minimal on purpose — override by shipping
|
|
3
3
|
app/views/kiosk/server/assistants/show.html.erb in the host app. %>
|
|
4
|
+
<% content_for :title, 'Linked assistants' %>
|
|
4
5
|
<main>
|
|
5
6
|
<h1>Linked assistant accounts</h1>
|
|
6
7
|
|
|
@@ -21,26 +22,17 @@
|
|
|
21
22
|
<section>
|
|
22
23
|
<h2>Link a new assistant</h2>
|
|
23
24
|
<p>
|
|
24
|
-
|
|
25
|
-
its own key and is then
|
|
25
|
+
Get a link code and paste it into your assistant's chat. The assistant
|
|
26
|
+
uses the code once, with its own key, and is then linked to your account.
|
|
26
27
|
</p>
|
|
27
28
|
<form action="<%= @page_path %>/link" method="post">
|
|
28
29
|
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
29
|
-
<button type="submit">
|
|
30
|
+
<button type="submit">Get a link code</button>
|
|
30
31
|
</form>
|
|
31
32
|
</section>
|
|
32
33
|
|
|
33
34
|
<section>
|
|
34
35
|
<h2>Currently linked</h2>
|
|
35
|
-
<% unless @spending_cap_enforced %>
|
|
36
|
-
<p role="note">
|
|
37
|
-
<strong>Note:</strong> this origin configures no <code>spending_cap</code>
|
|
38
|
-
seam, so a cap saved below is recorded against the assistant but nothing
|
|
39
|
-
reads it when a charge is settled. To make it bind, set
|
|
40
|
-
<code>c.spending_cap = Kiosk::Server::ColumnSpendingCap.new</code> in the
|
|
41
|
-
Kiosk initializer.
|
|
42
|
-
</p>
|
|
43
|
-
<% end %>
|
|
44
36
|
<% if @assistants.empty? %>
|
|
45
37
|
<p>No assistant accounts are linked yet.</p>
|
|
46
38
|
<% else %>
|
|
@@ -49,36 +41,40 @@
|
|
|
49
41
|
<li>
|
|
50
42
|
<strong><%= assistant[:human_label].to_s.empty? ? "(unnamed)" : assistant[:human_label] %></strong>
|
|
51
43
|
<code><%= assistant[:fingerprint] %></code>
|
|
52
|
-
|
|
44
|
+
<% if assistant[:bound_at] %>— linked <%= assistant[:bound_at] %><% end %>
|
|
45
|
+
<% if @payments %>
|
|
53
46
|
<p>
|
|
54
|
-
<%
|
|
55
|
-
|
|
56
|
-
%>
|
|
57
|
-
spent: <%= spent.empty? ? "0 cents" : spent.join(", ") %> —
|
|
47
|
+
<% spent = assistant[:spent_cents].map { |currency, cents| Kiosk::Server::Money.format(cents, currency) } %>
|
|
48
|
+
Spent: <%= spent.empty? ? "nothing" : spent.join(", ") %>
|
|
49
|
+
<% if @spending_cap %>
|
|
58
50
|
<%
|
|
59
51
|
cap = assistant[:spending_cap_cents]
|
|
60
52
|
cap_label = if cap.nil?
|
|
61
|
-
"cap
|
|
53
|
+
"no spending cap"
|
|
62
54
|
elsif cap.zero?
|
|
63
|
-
"
|
|
55
|
+
"no payments allowed"
|
|
64
56
|
else
|
|
65
|
-
"cap
|
|
57
|
+
"spending cap #{Kiosk::Server::Money.format(cap, assistant[:spending_cap_currency])} per payment"
|
|
66
58
|
end
|
|
67
59
|
%>
|
|
68
|
-
<%= cap_label %>
|
|
60
|
+
— <%= cap_label %>
|
|
61
|
+
<% end %>
|
|
69
62
|
</p>
|
|
63
|
+
<% end %>
|
|
70
64
|
<form action="<%= @page_path %>/update" method="post">
|
|
71
65
|
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
|
72
66
|
<input type="hidden" name="agent_id" value="<%= assistant[:agent_id] %>">
|
|
73
67
|
<label>Label
|
|
74
68
|
<input type="text" name="human_label" value="<%= assistant[:human_label] %>">
|
|
75
69
|
</label>
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
<% if @spending_cap %>
|
|
71
|
+
<label>Spending cap per payment (blank: no cap; 0: no payments)
|
|
72
|
+
<input type="text" name="spending_cap" inputmode="decimal" value="<%= assistant[:spending_cap_cents] && Kiosk::Server::Money.amount(assistant[:spending_cap_cents], assistant[:spending_cap_currency]) %>">
|
|
78
73
|
</label>
|
|
79
|
-
<label>Cap currency (
|
|
74
|
+
<label>Cap currency (a three-letter code such as EUR; the assistant cannot pay in another currency)
|
|
80
75
|
<input type="text" name="spending_cap_currency" maxlength="3" value="<%= assistant[:spending_cap_currency]&.upcase %>">
|
|
81
76
|
</label>
|
|
77
|
+
<% end %>
|
|
82
78
|
<button type="submit">Save</button>
|
|
83
79
|
</form>
|
|
84
80
|
<form action="<%= @page_path %>/unlink" method="post">
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<%# The document around the account holder's pages. Override by shipping
|
|
3
|
+
app/views/layouts/kiosk/server/application.html.erb in the host app. %>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title><%= yield :title %> — <%= request.host %></title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<%= yield %>
|
|
12
|
+
<footer><a href="/">Back to <%= request.host %></a></footer>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -6,24 +6,22 @@ require 'rails/generators/migration'
|
|
|
6
6
|
|
|
7
7
|
module Kiosk
|
|
8
8
|
module Generators
|
|
9
|
-
# `bin/rails g kiosk:install`: writes the Kiosk initializer, the
|
|
10
|
-
# and the migrations
|
|
9
|
+
# `bin/rails g kiosk:install`: writes the Kiosk initializer, the Kiosk routes
|
|
10
|
+
# and the migrations of Kiosk's `kiosk_*` tables.
|
|
11
11
|
class InstallGenerator < ::Rails::Generators::Base
|
|
12
12
|
include ::Rails::Generators::Migration
|
|
13
13
|
|
|
14
14
|
source_root File.expand_path('templates', __dir__)
|
|
15
15
|
|
|
16
16
|
desc 'Generate the Kiosk initializer and its canonical base migrations. ' \
|
|
17
|
-
'Draws the engine mount, so the origin answers
|
|
17
|
+
'Draws the engine mount, so the origin answers Kiosk requests.'
|
|
18
18
|
|
|
19
19
|
class_option :user_table, type: :string, default: 'users',
|
|
20
20
|
desc: "Provider's user table name"
|
|
21
|
-
class_option :user_id_type, type: :string, default: 'uuid',
|
|
21
|
+
class_option :user_id_type, type: :string, default: 'uuid', enum: %w[uuid bigint integer text],
|
|
22
22
|
desc: 'User-id column type: uuid | bigint | integer | text'
|
|
23
|
-
class_option :schema, type: :string, default: '
|
|
24
|
-
desc:
|
|
25
|
-
class_option :guc_namespace, type: :string, default: 'app',
|
|
26
|
-
desc: 'GUC namespace prefix used in the session GUC names'
|
|
23
|
+
class_option :schema, type: :string, default: 'public',
|
|
24
|
+
desc: "Postgres schema of Kiosk's tables"
|
|
27
25
|
|
|
28
26
|
# Ascending timestamps within one run, so the migrations sort in order.
|
|
29
27
|
@migration_counter = 0
|
|
@@ -36,23 +34,30 @@ module Kiosk
|
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
37
|
+
MIGRATIONS = %w[identity_tables device_authorizations mandates kyc_attributes events
|
|
38
|
+
pow_spent revocations visits refunds].freeze
|
|
39
|
+
|
|
39
40
|
no_commands do
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
41
|
+
def public_schema? = options[:schema] == 'public'
|
|
42
|
+
|
|
43
|
+
# A Kiosk table as a migration names it: `:kiosk_agents`, or `"ops.kiosk_agents"`.
|
|
44
|
+
def table(name) = public_schema? ? ":kiosk_#{name}" : %("#{options[:schema]}.kiosk_#{name}")
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
# The type of `users.id`, which every `user_id` column takes.
|
|
47
|
+
def user_id_type = ":#{options[:user_id_type]}"
|
|
48
|
+
|
|
49
|
+
# The foreign key of a `user_id` column: a row goes with its user.
|
|
50
|
+
def users_key = "{ to_table: :#{options[:user_table]}, on_delete: :cascade }"
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def create_initializer
|
|
50
54
|
template 'initializer.rb.tt', 'config/initializers/kiosk.rb'
|
|
51
55
|
end
|
|
52
56
|
|
|
53
|
-
# schema.rb dumps only the search path's schemas unless told otherwise
|
|
54
|
-
# public goes first because the kiosk tables reference the user table.
|
|
57
|
+
# schema.rb dumps only the search path's schemas unless told otherwise.
|
|
55
58
|
def dump_kiosk_schema
|
|
59
|
+
return if public_schema?
|
|
60
|
+
|
|
56
61
|
application %(config.active_record.dump_schemas = "public,#{options[:schema]}")
|
|
57
62
|
end
|
|
58
63
|
|
|
@@ -66,54 +71,10 @@ module Kiosk
|
|
|
66
71
|
RUBY
|
|
67
72
|
end
|
|
68
73
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def create_identity_tables_migration
|
|
75
|
-
migration_template 'create_kiosk_identity_tables.rb.tt',
|
|
76
|
-
'db/migrate/create_kiosk_identity_tables.rb'
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def create_device_authorizations_migration
|
|
80
|
-
migration_template 'create_kiosk_device_authorizations.rb.tt',
|
|
81
|
-
'db/migrate/create_kiosk_device_authorizations.rb'
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def create_mandates_migration
|
|
85
|
-
migration_template 'create_kiosk_mandates.rb.tt',
|
|
86
|
-
'db/migrate/create_kiosk_mandates.rb'
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def create_kyc_attributes_migration
|
|
90
|
-
migration_template 'create_kiosk_kyc_attributes.rb.tt',
|
|
91
|
-
'db/migrate/create_kiosk_kyc_attributes.rb'
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def create_events_migration
|
|
95
|
-
migration_template 'create_kiosk_events.rb.tt',
|
|
96
|
-
'db/migrate/create_kiosk_events.rb'
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def create_pow_spent_migration
|
|
100
|
-
migration_template 'create_kiosk_pow_spent.rb.tt',
|
|
101
|
-
'db/migrate/create_kiosk_pow_spent.rb'
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def create_revocations_migration
|
|
105
|
-
migration_template 'create_kiosk_revocations.rb.tt',
|
|
106
|
-
'db/migrate/create_kiosk_revocations.rb'
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def create_visits_migration
|
|
110
|
-
migration_template 'create_kiosk_visits.rb.tt',
|
|
111
|
-
'db/migrate/create_kiosk_visits.rb'
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def create_refunds_migration
|
|
115
|
-
migration_template 'create_kiosk_refunds.rb.tt',
|
|
116
|
-
'db/migrate/create_kiosk_refunds.rb'
|
|
74
|
+
def create_migrations
|
|
75
|
+
MIGRATIONS.each do |name|
|
|
76
|
+
migration_template "create_kiosk_#{name}.rb.tt", "db/migrate/create_kiosk_#{name}.rb"
|
|
77
|
+
end
|
|
117
78
|
end
|
|
118
79
|
end
|
|
119
80
|
end
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# user codes, the bound public key, claim vs link).
|
|
3
|
+
# The account-binding requests behind the claim and link ceremonies (RFC 8628
|
|
4
|
+
# shape). The device and user codes are stored hashed. `requested_role` is the
|
|
5
|
+
# role of the approving human, never one a client asks for.
|
|
7
6
|
class CreateKioskDeviceAuthorizations < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
def change
|
|
8
|
+
create_table <%= table :device_authorizations %>, id: :uuid do |t|
|
|
9
|
+
t.text :device_code_hash, null: false, index: { unique: true }
|
|
10
|
+
# Unique among pending requests only: a finished request may share a code with an old one.
|
|
11
|
+
t.text :user_code_hash, null: false, index: { unique: true, where: "status = 'pending'" }
|
|
12
|
+
t.text :public_key_pem
|
|
13
|
+
t.text :kind, null: false, default: 'claim'
|
|
14
|
+
t.text :client_id, null: false
|
|
15
|
+
t.text :requested_role
|
|
16
|
+
t.text :status, null: false
|
|
17
|
+
t.column :user_id, <%= user_id_type %>
|
|
18
|
+
t.timestamptz :expires_at, null: false, index: { where: "status IN ('pending', 'approved')" }
|
|
19
|
+
t.timestamptz :consumed_at
|
|
20
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }
|
|
21
|
+
t.check_constraint "status IN ('pending', 'approved', 'denied', 'consumed', 'expired')",
|
|
22
|
+
name: 'kiosk_device_authorizations_status'
|
|
23
|
+
t.check_constraint "kind IN ('claim', 'link')", name: 'kiosk_device_authorizations_kind'
|
|
24
|
+
end
|
|
16
25
|
end
|
|
17
26
|
end
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# integer — one cursor then resumes every subscription on a socket with one
|
|
7
|
-
# integer comparison.
|
|
8
|
-
# `identity_key` is a user_id, not an agent_id: a human's second assistant must
|
|
9
|
-
# read the same tail, and a revoked assistant is stopped at the socket.
|
|
10
|
-
# Laid down unconditionally, like kiosk.kyc_attributes: the route is drawn
|
|
11
|
-
# whether or not this origin declares a topic, and an empty table costs nothing.
|
|
3
|
+
# The event tail. `id` is the origin's one ascending cursor: a single integer
|
|
4
|
+
# resumes every subscription on a socket. `identity_key` is a user id, so every
|
|
5
|
+
# assistant of one person reads the same tail.
|
|
12
6
|
class CreateKioskEvents < ActiveRecord::Migration[ActiveRecord::Migration.current_version]
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
def change
|
|
8
|
+
create_table <%= table :events %> do |t|
|
|
9
|
+
t.text :identity_key, null: false
|
|
10
|
+
t.text :topic, null: false
|
|
11
|
+
t.text :subject
|
|
12
|
+
t.timestamptz :occurred_at, null: false
|
|
13
|
+
t.jsonb :data, null: false
|
|
14
|
+
# The retention sweep reads it.
|
|
15
|
+
t.timestamptz :created_at, null: false, default: -> { 'now()' }, index: true
|
|
16
|
+
# `since` for one subscriber: the cursor scan, in id order.
|
|
17
|
+
t.index %i[identity_key id]
|
|
18
|
+
end
|
|
21
19
|
end
|
|
22
20
|
end
|