open-loam 0.1.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 +7 -0
- data/CHANGELOG.md +71 -0
- data/LICENSE +21 -0
- data/README.md +177 -0
- data/app/jobs/loam/custom_field_reindex_job.rb +22 -0
- data/app/jobs/loam/event_delivery_job.rb +26 -0
- data/app/jobs/loam/event_redelivery_sweep_job.rb +21 -0
- data/app/jobs/loam/webhook_delivery_job.rb +59 -0
- data/app/models/loam/api_token.rb +40 -0
- data/app/models/loam/audit_record.rb +18 -0
- data/app/models/loam/auth_attempt.rb +13 -0
- data/app/models/loam/business_rule.rb +49 -0
- data/app/models/loam/business_rule_run.rb +27 -0
- data/app/models/loam/comment.rb +28 -0
- data/app/models/loam/config.rb +27 -0
- data/app/models/loam/custom_field_value.rb +18 -0
- data/app/models/loam/dashboard_widget.rb +16 -0
- data/app/models/loam/dictionary.rb +20 -0
- data/app/models/loam/dictionary_entry.rb +31 -0
- data/app/models/loam/event_delivery.rb +38 -0
- data/app/models/loam/field_definition.rb +48 -0
- data/app/models/loam/inbound_webhook_delivery.rb +24 -0
- data/app/models/loam/inbound_webhook_source.rb +55 -0
- data/app/models/loam/membership.rb +27 -0
- data/app/models/loam/mfa_credential.rb +114 -0
- data/app/models/loam/notification.rb +36 -0
- data/app/models/loam/pending_action.rb +208 -0
- data/app/models/loam/perspective.rb +104 -0
- data/app/models/loam/progress_job.rb +103 -0
- data/app/models/loam/record_lock.rb +19 -0
- data/app/models/loam/scheduled_job.rb +42 -0
- data/app/models/loam/search_token.rb +9 -0
- data/app/models/loam/sso_identity.rb +13 -0
- data/app/models/loam/sso_provider.rb +35 -0
- data/app/models/loam/tenant.rb +20 -0
- data/app/models/loam/translation.rb +17 -0
- data/app/models/loam/webhook_endpoint.rb +26 -0
- data/app/views/loam/attachments/_attachments.html.erb +16 -0
- data/app/views/loam/comments/_comments.html.erb +24 -0
- data/app/views/loam/custom_fields/_fields.html.erb +35 -0
- data/lib/generators/loam/entity/entity_generator.rb +160 -0
- data/lib/generators/loam/entity/templates/api_controller.rb +67 -0
- data/lib/generators/loam/entity/templates/controller.rb +181 -0
- data/lib/generators/loam/entity/templates/entity_test.rb +107 -0
- data/lib/generators/loam/entity/templates/migration.rb +26 -0
- data/lib/generators/loam/entity/templates/model.rb +27 -0
- data/lib/generators/loam/entity/templates/policy.rb +10 -0
- data/lib/generators/loam/entity/templates/views/_form.html.erb +63 -0
- data/lib/generators/loam/entity/templates/views/deleted.html.erb +41 -0
- data/lib/generators/loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/index.html.erb +90 -0
- data/lib/generators/loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/show.html.erb +36 -0
- data/lib/generators/loam/install/install_generator.rb +288 -0
- data/lib/generators/loam/install/templates/AGENTS.md +341 -0
- data/lib/generators/loam/install/templates/admin/api_docs_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/api_docs_index.html.erb +34 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +30 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_index.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/base_controller.rb +224 -0
- data/lib/generators/loam/install/templates/admin/business_rules_controller.rb +64 -0
- data/lib/generators/loam/install/templates/admin/business_rules_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/business_rules_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/business_rules_index.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/business_rules_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/comments_controller.rb +39 -0
- data/lib/generators/loam/install/templates/admin/configs_controller.rb +46 -0
- data/lib/generators/loam/install/templates/admin/configs_edit.html.erb +22 -0
- data/lib/generators/loam/install/templates/admin/configs_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +10 -0
- data/lib/generators/loam/install/templates/admin/dashboard_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_index.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_controller.rb +55 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_edit.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_form.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/dictionary_entries_controller.rb +47 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/events_controller.rb +42 -0
- data/lib/generators/loam/install/templates/admin/features_controller.rb +35 -0
- data/lib/generators/loam/install/templates/admin/features_index.html.erb +37 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_controller.rb +52 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_new.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/history_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/history_index.html.erb +35 -0
- data/lib/generators/loam/install/templates/admin/imports_controller.rb +75 -0
- data/lib/generators/loam/install/templates/admin/imports_new.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/imports_preview.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/imports_summary.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_new.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/loam/install/templates/admin/mfa_activated.html.erb +12 -0
- data/lib/generators/loam/install/templates/admin/mfa_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/mfa_new.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/mfa_show.html.erb +19 -0
- data/lib/generators/loam/install/templates/admin/notifications_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/notifications_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/overrides_controller.rb +14 -0
- data/lib/generators/loam/install/templates/admin/overrides_index.html.erb +29 -0
- data/lib/generators/loam/install/templates/admin/pagination.rb +23 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_index.html.erb +40 -0
- data/lib/generators/loam/install/templates/admin/perspectives_controller.rb +98 -0
- data/lib/generators/loam/install/templates/admin/perspectives_index.html.erb +38 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +19 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_index.html.erb +31 -0
- data/lib/generators/loam/install/templates/admin/record_locks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_controller.rb +69 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_edit.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_form.html.erb +16 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_index.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/search_controller.rb +49 -0
- data/lib/generators/loam/install/templates/admin/search_index.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/sessions_controller.rb +248 -0
- data/lib/generators/loam/install/templates/admin/sessions_mfa_challenge.html.erb +18 -0
- data/lib/generators/loam/install/templates/admin/sessions_new.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_controller.rb +65 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sudo_controller.rb +45 -0
- data/lib/generators/loam/install/templates/admin/sudo_new.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/translations_controller.rb +37 -0
- data/lib/generators/loam/install/templates/admin/translations_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_new.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin.css +29 -0
- data/lib/generators/loam/install/templates/api_base_controller.rb +90 -0
- data/lib/generators/loam/install/templates/guardrails_test.rb +79 -0
- data/lib/generators/loam/install/templates/import_job.rb +25 -0
- data/lib/generators/loam/install/templates/inbound_webhooks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/initializer.rb +218 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +14 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_auth_attempts.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_business_rules.rb +29 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_configs.rb +21 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +19 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionary_entries.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_event_deliveries.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_field_definitions.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_inbound_webhooks.rb +31 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_mfa_credentials.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_pending_actions.rb +26 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_perspectives.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_progress_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +16 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_scheduled_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_sso_providers.rb +34 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +9 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_users.rb +13 -0
- data/lib/generators/loam/install/templates/user.rb +21 -0
- data/lib/generators/loam/primary_key_options.rb +101 -0
- data/lib/loam/attachable.rb +27 -0
- data/lib/loam/auditable.rb +67 -0
- data/lib/loam/auth_throttle.rb +83 -0
- data/lib/loam/base32.rb +23 -0
- data/lib/loam/bulk.rb +59 -0
- data/lib/loam/business_rules/actions.rb +92 -0
- data/lib/loam/business_rules/condition.rb +102 -0
- data/lib/loam/business_rules.rb +123 -0
- data/lib/loam/commentable.rb +22 -0
- data/lib/loam/configs.rb +115 -0
- data/lib/loam/cron.rb +98 -0
- data/lib/loam/csv.rb +18 -0
- data/lib/loam/current.rb +21 -0
- data/lib/loam/custom_field_index.rb +262 -0
- data/lib/loam/custom_fields.rb +114 -0
- data/lib/loam/dashboard.rb +17 -0
- data/lib/loam/dictionaries.rb +55 -0
- data/lib/loam/durable_events.rb +174 -0
- data/lib/loam/encryptable.rb +145 -0
- data/lib/loam/encryption/cipher.rb +75 -0
- data/lib/loam/encryption/key_provider.rb +43 -0
- data/lib/loam/encryption.rb +125 -0
- data/lib/loam/engine.rb +57 -0
- data/lib/loam/enrichers.rb +105 -0
- data/lib/loam/errors.rb +49 -0
- data/lib/loam/eval.rb +47 -0
- data/lib/loam/event_stream.rb +80 -0
- data/lib/loam/eventful.rb +45 -0
- data/lib/loam/events.rb +51 -0
- data/lib/loam/export.rb +74 -0
- data/lib/loam/features.rb +75 -0
- data/lib/loam/generated_key.rb +37 -0
- data/lib/loam/import.rb +168 -0
- data/lib/loam/inbound_webhooks.rb +125 -0
- data/lib/loam/lifecycle.rb +180 -0
- data/lib/loam/locales/loam.en.yml +80 -0
- data/lib/loam/mcp/server.rb +60 -0
- data/lib/loam/mcp.rb +304 -0
- data/lib/loam/notifications.rb +36 -0
- data/lib/loam/open_api.rb +194 -0
- data/lib/loam/overrides.rb +110 -0
- data/lib/loam/pending_actions.rb +60 -0
- data/lib/loam/permissions.rb +79 -0
- data/lib/loam/perspectives.rb +62 -0
- data/lib/loam/policy.rb +106 -0
- data/lib/loam/progress.rb +31 -0
- data/lib/loam/record_locks.rb +89 -0
- data/lib/loam/scheduler.rb +153 -0
- data/lib/loam/search/driver.rb +31 -0
- data/lib/loam/search/like_driver.rb +29 -0
- data/lib/loam/search/token_driver.rb +88 -0
- data/lib/loam/search.rb +27 -0
- data/lib/loam/searchable.rb +90 -0
- data/lib/loam/soft_deletable.rb +72 -0
- data/lib/loam/sso/claims.rb +10 -0
- data/lib/loam/sso/fake_provider.rb +51 -0
- data/lib/loam/sso/http_client.rb +42 -0
- data/lib/loam/sso/oidc_provider.rb +64 -0
- data/lib/loam/sso.rb +129 -0
- data/lib/loam/telemetry.rb +43 -0
- data/lib/loam/tenant_record.rb +36 -0
- data/lib/loam/test_helpers.rb +15 -0
- data/lib/loam/totp.rb +63 -0
- data/lib/loam/translatable.rb +88 -0
- data/lib/loam/undo.rb +126 -0
- data/lib/loam/version.rb +3 -0
- data/lib/loam/webhooks.rb +34 -0
- data/lib/loam/widgets.rb +77 -0
- data/lib/loam/workflow.rb +256 -0
- data/lib/loam.rb +122 -0
- data/lib/open-loam.rb +5 -0
- data/lib/tasks/loam.rake +171 -0
- data/lib/tasks/loam_eval.rake +32 -0
- data/lib/tasks/loam_mcp.rake +20 -0
- metadata +332 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<h1>Dry run — <%%= @entity_type %></h1>
|
|
2
|
+
|
|
3
|
+
<p>Nothing was committed. This is what an import WOULD do:</p>
|
|
4
|
+
<ul>
|
|
5
|
+
<li>Create: <strong><%%= @result.created %></strong></li>
|
|
6
|
+
<li>Update: <strong><%%= @result.updated %></strong></li>
|
|
7
|
+
<li>Failed: <strong><%%= @result.failed %></strong></li>
|
|
8
|
+
</ul>
|
|
9
|
+
|
|
10
|
+
<%% if @result.errors.any? %>
|
|
11
|
+
<h2>Failed rows</h2>
|
|
12
|
+
<table>
|
|
13
|
+
<thead><tr><th>Row</th><th>Reason</th></tr></thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<%% @result.errors.each do |error| %>
|
|
16
|
+
<tr><td><%%= error["row"] %></td><td><%%= error["message"] %></td></tr>
|
|
17
|
+
<%% end %>
|
|
18
|
+
</tbody>
|
|
19
|
+
</table>
|
|
20
|
+
|
|
21
|
+
<%%= form_with url: download_errors_admin_imports_path, method: :post do %>
|
|
22
|
+
<%%= hidden_field_tag :entity_type, @entity_type %>
|
|
23
|
+
<%%= hidden_field_tag :csv, @csv %>
|
|
24
|
+
<%% params[:mapping].to_unsafe_h.each do |header, target| %>
|
|
25
|
+
<%%= hidden_field_tag "mapping[#{header}]", target %>
|
|
26
|
+
<%% end %>
|
|
27
|
+
<%%= hidden_field_tag :match_key, params[:match_key] %>
|
|
28
|
+
<%%= submit_tag "Download error file" %>
|
|
29
|
+
<%% end %>
|
|
30
|
+
<%% end %>
|
|
31
|
+
|
|
32
|
+
<p><%%= link_to "Back", new_admin_import_path(entity_type: @entity_type) %></p>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Manage the external systems allowed to POST webhooks into this tenant
|
|
3
|
+
# (Loam::InboundWebhookSource) — manager-only. Create a source (token + secret
|
|
4
|
+
# are generated), copy the receive URL and secret to the sender, rotate either,
|
|
5
|
+
# toggle active, and see the most recent received deliveries.
|
|
6
|
+
class InboundWebhookSourcesController < BaseController
|
|
7
|
+
before_action { require_role!(:manager) }
|
|
8
|
+
before_action :set_source, only: %i[destroy rotate_secret rotate_token toggle]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@sources = Loam::InboundWebhookSource.order(:name)
|
|
12
|
+
@recent = Loam::InboundWebhookDelivery.order(received_at: :desc).limit(20)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def new
|
|
16
|
+
@source = Loam::InboundWebhookSource.new(event_name: "inbound.webhook.received")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create
|
|
20
|
+
@source = Loam::InboundWebhookSource.new(source_params)
|
|
21
|
+
if @source.save
|
|
22
|
+
redirect_to admin_inbound_webhook_sources_path, notice: "Source created. Copy the secret now — it signs every call."
|
|
23
|
+
else
|
|
24
|
+
render :new, status: :unprocessable_entity
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def destroy
|
|
29
|
+
@source.destroy!
|
|
30
|
+
redirect_to admin_inbound_webhook_sources_path, notice: "Source deleted."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def rotate_secret
|
|
34
|
+
@source.rotate_secret!
|
|
35
|
+
redirect_to admin_inbound_webhook_sources_path, notice: "Secret rotated — update the sender."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rotate_token
|
|
39
|
+
@source.rotate_token!
|
|
40
|
+
redirect_to admin_inbound_webhook_sources_path, notice: "Token rotated — update the sender's URL."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def toggle
|
|
44
|
+
@source.update!(active: !@source.active?)
|
|
45
|
+
redirect_to admin_inbound_webhook_sources_path, notice: "Source #{@source.active? ? 'enabled' : 'disabled'}."
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_source
|
|
51
|
+
@source = Loam::InboundWebhookSource.find(params[:id])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def source_params
|
|
55
|
+
params.require(:inbound_webhook_source).permit(
|
|
56
|
+
:name, :event_name, :signature_header, :delivery_id_header,
|
|
57
|
+
:timestamp_header, :timestamp_tolerance
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<h1>Inbound webhooks</h1>
|
|
2
|
+
|
|
3
|
+
<p>External systems allowed to POST webhooks into this tenant. The <strong>URL</strong>
|
|
4
|
+
identifies a source; the <strong>secret</strong> (HMAC-SHA256 over the raw body,
|
|
5
|
+
sent in the signature header) authenticates each call. A verified delivery is
|
|
6
|
+
published on the event bus as its <code>event_name</code>.
|
|
7
|
+
<%%= link_to "New source", new_admin_inbound_webhook_source_path %></p>
|
|
8
|
+
|
|
9
|
+
<table>
|
|
10
|
+
<thead>
|
|
11
|
+
<tr><th>Name</th><th>Receive URL</th><th>Event</th><th>Active</th><th></th></tr>
|
|
12
|
+
</thead>
|
|
13
|
+
<tbody>
|
|
14
|
+
<%% @sources.each do |s| %>
|
|
15
|
+
<tr>
|
|
16
|
+
<td><%%= s.name %></td>
|
|
17
|
+
<td><code><%%= "#{request.base_url}/webhooks/#{s.token}" %></code><br>
|
|
18
|
+
<small>secret: <code><%%= s.secret %></code></small></td>
|
|
19
|
+
<td><code><%%= s.event_name %></code></td>
|
|
20
|
+
<td><%%= s.active? ? "yes" : "no" %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%%= button_to "Rotate token", rotate_token_admin_inbound_webhook_source_path(s), form: { style: "display:inline" } %>
|
|
23
|
+
<%%= button_to "Rotate secret", rotate_secret_admin_inbound_webhook_source_path(s), form: { style: "display:inline" } %>
|
|
24
|
+
<%%= button_to (s.active? ? "Disable" : "Enable"), toggle_admin_inbound_webhook_source_path(s), form: { style: "display:inline" } %>
|
|
25
|
+
<%%= button_to "Delete", admin_inbound_webhook_source_path(s), method: :delete, form: { style: "display:inline" } %>
|
|
26
|
+
</td>
|
|
27
|
+
</tr>
|
|
28
|
+
<%% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<h2>Recent deliveries</h2>
|
|
33
|
+
<%% if @recent.any? %>
|
|
34
|
+
<table>
|
|
35
|
+
<thead><tr><th>Source</th><th>Event</th><th>External id</th><th>Received</th></tr></thead>
|
|
36
|
+
<tbody>
|
|
37
|
+
<%% @recent.each do |d| %>
|
|
38
|
+
<tr>
|
|
39
|
+
<td><%%= d.source.name %></td>
|
|
40
|
+
<td><code><%%= d.event_name %></code></td>
|
|
41
|
+
<td><small><%%= truncate(d.external_id, length: 24) %></small></td>
|
|
42
|
+
<td><%%= d.received_at&.strftime("%Y-%m-%d %H:%M UTC") %></td>
|
|
43
|
+
</tr>
|
|
44
|
+
<%% end %>
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
<%% else %>
|
|
48
|
+
<p>No deliveries received yet.</p>
|
|
49
|
+
<%% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<h1>New inbound webhook source</h1>
|
|
2
|
+
<p><%%= link_to "Back", admin_inbound_webhook_sources_path %></p>
|
|
3
|
+
|
|
4
|
+
<%%= form_with model: @source, url: admin_inbound_webhook_sources_path, method: :post do |f| %>
|
|
5
|
+
<%% if @source.errors.any? %>
|
|
6
|
+
<ul><%% @source.errors.full_messages.each do |m| %><li><%%= m %></li><%% end %></ul>
|
|
7
|
+
<%% end %>
|
|
8
|
+
<p>
|
|
9
|
+
<%%= f.label :name %><br>
|
|
10
|
+
<%%= f.text_field :name %>
|
|
11
|
+
</p>
|
|
12
|
+
<p>
|
|
13
|
+
<%%= f.label :event_name, "Publish as event (domain.thing.happened)" %><br>
|
|
14
|
+
<%%= f.text_field :event_name %>
|
|
15
|
+
</p>
|
|
16
|
+
<p>
|
|
17
|
+
<%%= f.label :signature_header, "Signature header (default X-Loam-Signature)" %><br>
|
|
18
|
+
<%%= f.text_field :signature_header, placeholder: "X-Loam-Signature" %>
|
|
19
|
+
<small>e.g. X-Hub-Signature-256 for GitHub.</small>
|
|
20
|
+
</p>
|
|
21
|
+
<p>
|
|
22
|
+
<%%= f.label :delivery_id_header, "Delivery-id header (optional, for dedupe)" %><br>
|
|
23
|
+
<%%= f.text_field :delivery_id_header %>
|
|
24
|
+
<small>Blank = dedupe on a hash of the body.</small>
|
|
25
|
+
</p>
|
|
26
|
+
<p>
|
|
27
|
+
<%%= f.label :timestamp_header, "Timestamp header (optional, enables freshness window)" %><br>
|
|
28
|
+
<%%= f.text_field :timestamp_header %>
|
|
29
|
+
<%%= f.number_field :timestamp_tolerance, placeholder: "300 (seconds)" %>
|
|
30
|
+
</p>
|
|
31
|
+
<%%= f.submit "Create source" %>
|
|
32
|
+
<%% end %>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Admin</title>
|
|
5
|
+
<%%= csrf_meta_tags %>
|
|
6
|
+
<%%= stylesheet_link_tag "admin", media: "all" %>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<%% flash.each do |type, message| %>
|
|
10
|
+
<div class="flash flash-<%%= type.to_s == "alert" ? "alert" : "notice" %>"><%%= message %></div>
|
|
11
|
+
<%% end %>
|
|
12
|
+
<%% if defined?(current_tenant) && current_tenant %>
|
|
13
|
+
<div class="context">
|
|
14
|
+
<%%= t("loam.chrome.tenant") %>: <strong><%%= current_tenant.name %></strong> ·
|
|
15
|
+
<%%= t("loam.chrome.user") %>: <strong><%%= current_actor&.name %></strong> ·
|
|
16
|
+
<%%= button_to t("loam.chrome.sign_out"), admin_session_path, method: :delete, form: { style: "display:inline" } %>
|
|
17
|
+
</div>
|
|
18
|
+
<nav>
|
|
19
|
+
<%%= link_to t("loam.nav.dashboard"), admin_root_path %>
|
|
20
|
+
<%%= link_to admin_notifications_path do %><%%= t("loam.nav.notifications") %> (<span id="loam-notification-count"><%%= unread_notification_count %></span>)<%% end %>
|
|
21
|
+
<%%= link_to t("loam.nav.api_tokens"), admin_api_tokens_path %>
|
|
22
|
+
<%%= link_to t("loam.nav.security"), admin_mfa_path %>
|
|
23
|
+
<%%= link_to "#{t('loam.nav.approvals')} (#{pending_approval_count})", admin_pending_actions_path %>
|
|
24
|
+
<%%= link_to t("loam.nav.settings"), admin_configs_path %>
|
|
25
|
+
<%%= link_to t("loam.nav.features"), admin_features_path %>
|
|
26
|
+
<%%= link_to t("loam.nav.rules"), admin_business_rules_path %>
|
|
27
|
+
<%%= link_to t("loam.nav.sso"), admin_sso_providers_path %>
|
|
28
|
+
<%%= link_to t("loam.nav.dictionaries"), admin_dictionaries_path %>
|
|
29
|
+
<%%= link_to t("loam.nav.tasks"), admin_progress_jobs_path %>
|
|
30
|
+
<%%= link_to t("loam.nav.schedules"), admin_scheduled_jobs_path %>
|
|
31
|
+
<%%= link_to t("loam.nav.deliveries"), admin_event_deliveries_path %>
|
|
32
|
+
<%%= link_to t("loam.nav.inbound"), admin_inbound_webhook_sources_path %>
|
|
33
|
+
<%%= link_to t("loam.nav.api_docs"), admin_api_docs_path %>
|
|
34
|
+
<%%= link_to t("loam.nav.overrides"), admin_overrides_path %>
|
|
35
|
+
<%%= form_with url: admin_search_path, method: :get, class: "search" do %>
|
|
36
|
+
<%%= text_field_tag :q, params[:q], placeholder: t("loam.chrome.search") %>
|
|
37
|
+
<%% end %>
|
|
38
|
+
<%%# Locale switcher (Loam::Translatable): overlays content onto the picked language. %>
|
|
39
|
+
<%% if Loam.locales.size > 1 %>
|
|
40
|
+
<span class="locale">
|
|
41
|
+
<%% Loam.locales.each do |loc| %><%%= link_to loc.upcase, "#{request.path}?locale=#{loc}", style: (loc == current_locale ? "font-weight:bold" : "") %> <%% end %>
|
|
42
|
+
</span>
|
|
43
|
+
<%% end %>
|
|
44
|
+
</nav>
|
|
45
|
+
|
|
46
|
+
<%%# Live updates over SSE (Loam::EventStream). Degrades gracefully: if
|
|
47
|
+
EventSource is unavailable or the stream drops, the per-page-load bell
|
|
48
|
+
count above still works. %>
|
|
49
|
+
<script>
|
|
50
|
+
(function () {
|
|
51
|
+
if (typeof EventSource === "undefined") return;
|
|
52
|
+
var source = new EventSource("<%%= admin_events_stream_path %>");
|
|
53
|
+
source.addEventListener("loam.notification.created", function () {
|
|
54
|
+
var badge = document.getElementById("loam-notification-count");
|
|
55
|
+
if (badge) badge.textContent = (parseInt(badge.textContent, 10) || 0) + 1;
|
|
56
|
+
});
|
|
57
|
+
// Live task progress (Loam::ProgressJob): move the matching bar. The
|
|
58
|
+
// frame carries only id/percent/status.
|
|
59
|
+
source.addEventListener("loam.progress.updated", function (e) {
|
|
60
|
+
var d = JSON.parse(e.data);
|
|
61
|
+
var fill = document.getElementById("progress-fill-" + d.id);
|
|
62
|
+
if (fill) fill.style.width = d.percent + "%";
|
|
63
|
+
var pct = document.getElementById("progress-percent-" + d.id);
|
|
64
|
+
if (pct) pct.textContent = d.percent + "%";
|
|
65
|
+
var st = document.getElementById("progress-status-" + d.id);
|
|
66
|
+
if (st) st.textContent = d.status;
|
|
67
|
+
});
|
|
68
|
+
source.onerror = function () {
|
|
69
|
+
if (source.readyState === EventSource.CLOSED) source.close();
|
|
70
|
+
};
|
|
71
|
+
})();
|
|
72
|
+
</script>
|
|
73
|
+
<%% end %>
|
|
74
|
+
<%%= yield %>
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<h1>Two-factor authentication is on</h1>
|
|
2
|
+
|
|
3
|
+
<p><strong>Save these recovery codes now.</strong> Each works once if you lose
|
|
4
|
+
your authenticator. This is the only time they are shown.</p>
|
|
5
|
+
|
|
6
|
+
<ul>
|
|
7
|
+
<%% @recovery_codes.each do |code| %>
|
|
8
|
+
<li><code><%%= code %></code></li>
|
|
9
|
+
<%% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
|
|
12
|
+
<p><%%= link_to "Done", admin_mfa_path %></p>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# A user's own two-factor setup — self-service, so NOT role-gated (everyone
|
|
3
|
+
# manages their own MFA). Enrollment is two steps: `new` (GET) shows a
|
|
4
|
+
# CANDIDATE secret held in the session, `create` (POST) confirms a live code
|
|
5
|
+
# and only then adopts it. Crucially, GET never mutates the credential, and the
|
|
6
|
+
# old secret stays valid until the new one is proven — so neither a cross-site
|
|
7
|
+
# GET nor an abandoned re-enrollment can downgrade an active credential.
|
|
8
|
+
class MfaController < BaseController
|
|
9
|
+
# Shown in authenticator apps as the account issuer; defaults to the app name.
|
|
10
|
+
ISSUER = Rails.application.class.module_parent_name.freeze
|
|
11
|
+
|
|
12
|
+
# Replacing an ACTIVE credential (or disabling it) is sensitive, so gate those
|
|
13
|
+
# writes behind step-up. Initial enrollment (no active MFA yet) needs no sudo
|
|
14
|
+
# — there is nothing to protect and no second factor to prove.
|
|
15
|
+
before_action :require_sudo!, only: %i[create destroy], if: :active_mfa?
|
|
16
|
+
|
|
17
|
+
def show
|
|
18
|
+
@credential = Loam::MfaCredential.active_for(current_actor)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# READ-ONLY: mint a candidate secret into the (encrypted cookie) session and
|
|
22
|
+
# display it. No credential is touched, so this is safe to reach via GET.
|
|
23
|
+
def new
|
|
24
|
+
set_enrollment_secret
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Confirm the candidate against a live code, then adopt it and reveal the
|
|
28
|
+
# recovery codes once. The active secret is replaced only on success.
|
|
29
|
+
def create
|
|
30
|
+
secret = session[:mfa_enrollment_secret]
|
|
31
|
+
credential = Loam::MfaCredential.find_or_initialize_by(user_id: current_actor.id)
|
|
32
|
+
@recovery_codes = secret && credential.activate_with!(secret, params[:code])
|
|
33
|
+
|
|
34
|
+
if @recovery_codes
|
|
35
|
+
session.delete(:mfa_enrollment_secret)
|
|
36
|
+
render :activated
|
|
37
|
+
else
|
|
38
|
+
set_enrollment_secret
|
|
39
|
+
@error = "That code did not match. Check your authenticator and try again."
|
|
40
|
+
render :new, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def destroy
|
|
45
|
+
Loam::MfaCredential.where(user_id: current_actor.id).delete_all
|
|
46
|
+
session.delete(:mfa_enrollment_secret)
|
|
47
|
+
redirect_to admin_mfa_path, notice: "Two-factor authentication disabled."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def set_enrollment_secret
|
|
53
|
+
@secret = (session[:mfa_enrollment_secret] ||= Loam::Totp.generate_secret)
|
|
54
|
+
@provisioning_uri = Loam::Totp.provisioning_uri(@secret, account: current_actor.email, issuer: ISSUER)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def active_mfa?
|
|
58
|
+
Loam::MfaCredential.active_for(current_actor).present?
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<%% if @error %>
|
|
2
|
+
<p style="color:#b00"><%%= @error %></p>
|
|
3
|
+
<%% end %>
|
|
4
|
+
|
|
5
|
+
<h1>Set up two-factor authentication</h1>
|
|
6
|
+
|
|
7
|
+
<p>Add this account to your authenticator app, then confirm a code to activate.
|
|
8
|
+
Scan the QR (a later nicety) or enter the details by hand:</p>
|
|
9
|
+
|
|
10
|
+
<ul>
|
|
11
|
+
<li>Secret: <code><%%= @secret %></code></li>
|
|
12
|
+
<li>otpauth URI: <code><%%= @provisioning_uri %></code></li>
|
|
13
|
+
</ul>
|
|
14
|
+
|
|
15
|
+
<%%= form_with url: admin_mfa_path, method: :post do |f| %>
|
|
16
|
+
<p>
|
|
17
|
+
<%%= label_tag :code, "Code from your app" %><br>
|
|
18
|
+
<%%= text_field_tag :code, nil, autofocus: true, autocomplete: "one-time-code", inputmode: "numeric" %>
|
|
19
|
+
</p>
|
|
20
|
+
<%%= f.submit "Activate" %>
|
|
21
|
+
<%% end %>
|
|
22
|
+
|
|
23
|
+
<p><%%= link_to "Cancel", admin_mfa_path %></p>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<h1>Two-factor authentication</h1>
|
|
2
|
+
|
|
3
|
+
<%% if @credential %>
|
|
4
|
+
<p>Status: <strong>enabled</strong> — activated <%%= @credential.activated_at.to_date %>.</p>
|
|
5
|
+
<p>Unused recovery codes: <strong><%%= @credential.unused_recovery_code_count %></strong>.</p>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<%%= link_to "Re-enroll (new secret + codes)", new_admin_mfa_path %>
|
|
9
|
+
</p>
|
|
10
|
+
<%%# Disabling MFA is sensitive, so it is step-up gated (require_sudo!). %>
|
|
11
|
+
<%%= button_to "Disable two-factor", admin_mfa_path, method: :delete,
|
|
12
|
+
data: { turbo_confirm: "Disable two-factor authentication for your account?" } %>
|
|
13
|
+
<%% else %>
|
|
14
|
+
<p>Status: <strong>not enabled</strong>. Protect your account with an
|
|
15
|
+
authenticator app.</p>
|
|
16
|
+
<p><%%= link_to "Set up two-factor authentication", new_admin_mfa_path %></p>
|
|
17
|
+
<%% end %>
|
|
18
|
+
|
|
19
|
+
<p><%%= link_to "Back to dashboard", admin_root_path %></p>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# The bell. Every query here is scoped to the current actor, so there is no
|
|
3
|
+
# "read someone else's notifications" path to authorize and no per-record
|
|
4
|
+
# policy to write: tenancy is structural (Loam::Notification is a
|
|
5
|
+
# Loam::TenantRecord) and BaseController has already established that this
|
|
6
|
+
# actor is signed in to this tenant. Recipient scoping is the whole rule.
|
|
7
|
+
class NotificationsController < BaseController
|
|
8
|
+
def index
|
|
9
|
+
@records = notifications.order(created_at: :desc).limit(100)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mark_read
|
|
13
|
+
notifications.find(params[:id]).mark_read!
|
|
14
|
+
redirect_to admin_notifications_path
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def notifications
|
|
20
|
+
Loam::Notification.where(user_id: current_actor.id)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<h1>Notifications</h1>
|
|
2
|
+
|
|
3
|
+
<p>Messages for <strong><%%= current_actor.name %></strong> in <strong><%%= current_tenant.name %></strong>.</p>
|
|
4
|
+
|
|
5
|
+
<table>
|
|
6
|
+
<thead>
|
|
7
|
+
<tr>
|
|
8
|
+
<th>When</th>
|
|
9
|
+
<th>Title</th>
|
|
10
|
+
<th>About</th>
|
|
11
|
+
<th></th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody>
|
|
15
|
+
<%% @records.each do |record| %>
|
|
16
|
+
<tr>
|
|
17
|
+
<td><%%= record.created_at.to_fs(:short) %></td>
|
|
18
|
+
<td>
|
|
19
|
+
<%%= tag.strong record.title unless record.read? %>
|
|
20
|
+
<%%= record.title if record.read? %>
|
|
21
|
+
<%%= tag.div record.body, style: "color:#555" if record.body.present? %>
|
|
22
|
+
</td>
|
|
23
|
+
<td><%%= "#{record.source_type} ##{record.source_id}" if record.source_type %></td>
|
|
24
|
+
<td>
|
|
25
|
+
<%% unless record.read? %>
|
|
26
|
+
<%%= button_to "Mark read", mark_read_admin_notification_path(record), form: { style: "display:inline" } %>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Read-only view of the app's Loam::Overrides — which registry entries are
|
|
3
|
+
# disabled or replaced, plus any STALE overrides (a key that no longer exists).
|
|
4
|
+
# Manager-only, for legibility: it's config declared in an initializer, shown
|
|
5
|
+
# here so an operator can see what's been customized without reading code.
|
|
6
|
+
class OverridesController < BaseController
|
|
7
|
+
before_action { require_role!(:manager) }
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@overrides = Loam::Overrides.all
|
|
11
|
+
@stale = Loam::Overrides.stale
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<h1>Overrides</h1>
|
|
2
|
+
|
|
3
|
+
<p>Registry entries this app disables or replaces (declared in
|
|
4
|
+
<code>config/initializers/loam.rb</code> via <code>Loam::Overrides</code>) —
|
|
5
|
+
customization without forking. Structural pieces (views, controllers, routes)
|
|
6
|
+
are overridden the Rails way (path shadowing), not here.</p>
|
|
7
|
+
|
|
8
|
+
<%% if @stale.any? %>
|
|
9
|
+
<p style="color:#b00"><strong>Stale overrides</strong> (the key no longer exists — the override does nothing):
|
|
10
|
+
<%%= @stale.map { |reg, key| "#{reg}/#{key}" }.join(", ") %></p>
|
|
11
|
+
<%% end %>
|
|
12
|
+
|
|
13
|
+
<table>
|
|
14
|
+
<thead><tr><th>Registry</th><th>Key</th><th>Effect</th></tr></thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<%% @overrides.each do |registry, entries| %>
|
|
17
|
+
<%% entries.each do |key, value| %>
|
|
18
|
+
<tr>
|
|
19
|
+
<td><code><%%= registry %></code></td>
|
|
20
|
+
<td><code><%%= key %></code></td>
|
|
21
|
+
<td><%%= value == Loam::Overrides::DISABLED ? "disabled" : "replaced" %></td>
|
|
22
|
+
</tr>
|
|
23
|
+
<%% end %>
|
|
24
|
+
<%% end %>
|
|
25
|
+
<%% if @overrides.values.all?(&:empty?) %>
|
|
26
|
+
<tr><td colspan="3"><em>No overrides declared.</em></td></tr>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</tbody>
|
|
29
|
+
</table>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# Pagination with no gem attached — the smallest thing that works:
|
|
3
|
+
#
|
|
4
|
+
# @records, @page, @has_next = paginate(Equipment.order(created_at: :desc))
|
|
5
|
+
#
|
|
6
|
+
# Swap in pagy or kaminari when you need total counts, numbered pages or a
|
|
7
|
+
# window of links; callers only ever use these three values, so the change
|
|
8
|
+
# stays inside this module and the index views.
|
|
9
|
+
module Pagination
|
|
10
|
+
PER_PAGE = 25
|
|
11
|
+
|
|
12
|
+
def paginate(scope, per_page: PER_PAGE)
|
|
13
|
+
page = params[:page].to_i
|
|
14
|
+
page = 1 if page < 1
|
|
15
|
+
|
|
16
|
+
# One row more than a page is fetched and never rendered: it answers
|
|
17
|
+
# "is there a next page?" without a second COUNT over the whole table.
|
|
18
|
+
records = scope.limit(per_page + 1).offset((page - 1) * per_page).to_a
|
|
19
|
+
|
|
20
|
+
[ records.first(per_page), page, records.size > per_page ]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
# The approval queue: staged mutations (Loam::PendingAction) waiting on a
|
|
3
|
+
# human. Manager-only — approving EXECUTES the change. The workflow's role gate
|
|
4
|
+
# is the deeper enforcement (approve!/reject! raise for a non-manager); this
|
|
5
|
+
# screen is a manager screen on top of it.
|
|
6
|
+
class PendingActionsController < BaseController
|
|
7
|
+
before_action { require_role!(:manager) }
|
|
8
|
+
before_action :set_pending_action, only: %i[approve reject]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@pending = Loam::PendingAction.pending.order(created_at: :desc)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def approve
|
|
15
|
+
@pending_action.approve!(by: current_actor)
|
|
16
|
+
redirect_to admin_pending_actions_path, notice: approval_notice(@pending_action)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reject
|
|
20
|
+
@pending_action.reject!(by: current_actor, reason: params[:reason])
|
|
21
|
+
redirect_to admin_pending_actions_path, notice: "Rejected: #{@pending_action.summary}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def set_pending_action
|
|
27
|
+
@pending_action = Loam::PendingAction.find(params[:id])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def approval_notice(pending)
|
|
31
|
+
if pending.executed?
|
|
32
|
+
"Approved and applied: #{pending.summary}"
|
|
33
|
+
else
|
|
34
|
+
"Approved, but execution failed: #{pending.error}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<h1>Approvals</h1>
|
|
2
|
+
|
|
3
|
+
<p>Changes an agent (or any confirm-mode caller) staged for review. Approving
|
|
4
|
+
executes the change; nothing has been applied yet.</p>
|
|
5
|
+
|
|
6
|
+
<%% if @pending.empty? %>
|
|
7
|
+
<p>Nothing waiting for review.</p>
|
|
8
|
+
<%% end %>
|
|
9
|
+
|
|
10
|
+
<%% @pending.each do |pending| %>
|
|
11
|
+
<section style="border:1px solid #ccc; padding:1rem; margin-bottom:1rem">
|
|
12
|
+
<h2><%%= pending.summary %></h2>
|
|
13
|
+
<p>
|
|
14
|
+
<%%= pending.action_type %> on <%%= pending.target_type %><%%= " ##{pending.target_id}" if pending.target_id %> ·
|
|
15
|
+
staged by <%%= User.find_by(id: pending.actor_id)&.name || "—" %>
|
|
16
|
+
on <%%= pending.created_at.to_date %>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<table>
|
|
20
|
+
<thead>
|
|
21
|
+
<tr><th>Field</th><th>From</th><th>To</th></tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
<%% pending.preview.each do |field, change| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td><%%= field %></td>
|
|
27
|
+
<td><%%= change["from"].nil? ? "—" : change["from"] %></td>
|
|
28
|
+
<td><%%= change["to"] %></td>
|
|
29
|
+
</tr>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
33
|
+
|
|
34
|
+
<%%= button_to "Approve", approve_admin_pending_action_path(pending), form: { style: "display:inline" } %>
|
|
35
|
+
<%%= form_with url: reject_admin_pending_action_path(pending), method: :post, class: "reject" do %>
|
|
36
|
+
<%%= text_field_tag :reason, nil, placeholder: "Reason (optional)" %>
|
|
37
|
+
<%%= submit_tag "Reject" %>
|
|
38
|
+
<%% end %>
|
|
39
|
+
</section>
|
|
40
|
+
<%% end %>
|