seams 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +212 -12
- data/README.md +235 -82
- data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
- data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
- data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
- data/lib/generators/seams/admin/admin_generator.rb +51 -4
- data/lib/generators/seams/admin/templates/README.md.tt +33 -3
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
- data/lib/generators/seams/auth/auth_generator.rb +1 -2
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/billing/billing_generator.rb +4 -3
- data/lib/generators/seams/billing/templates/README.md.tt +26 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
- data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
- data/lib/generators/seams/design/design_generator.rb +647 -0
- data/lib/generators/seams/design/templates/README.md.tt +129 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
- data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
- data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
- data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
- data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
- data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
- data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
- data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
- data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
- data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
- data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
- data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
- data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
- data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
- data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
- data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
- data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
- data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
- data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
- data/lib/generators/seams/engine/engine_generator.rb +6 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
- data/lib/generators/seams/install/install_generator.rb +72 -6
- data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
- data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
- data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
- data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
- data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
- data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
- data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
- data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
- data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
- data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
- data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
- data/lib/seams/cli/list.rb +15 -0
- data/lib/seams/cli/quality.rb +46 -5
- data/lib/seams/cli/resolve.rb +9 -5
- data/lib/seams/cli/test_changed.rb +5 -1
- data/lib/seams/cli.rb +20 -0
- data/lib/seams/configuration.rb +24 -1
- data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
- data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
- data/lib/seams/event_registry.rb +19 -0
- data/lib/seams/events/adapter.rb +9 -0
- data/lib/seams/events/adapters/active_support.rb +4 -0
- data/lib/seams/events/publisher.rb +29 -0
- data/lib/seams/events.rb +1 -0
- data/lib/seams/generators/dummy_app_writer.rb +21 -0
- data/lib/seams/generators/follow_up_generator.rb +1 -2
- data/lib/seams/generators/host_injector.rb +9 -2
- data/lib/seams/generators/splicer.rb +2 -2
- data/lib/seams/observability/adapter.rb +22 -0
- data/lib/seams/observability.rb +6 -0
- data/lib/seams/permission_registry.rb +66 -0
- data/lib/seams/permissions.rb +120 -0
- data/lib/seams/version.rb +1 -1
- data/lib/seams.rb +7 -0
- metadata +107 -3
|
@@ -77,8 +77,10 @@ Seams::Admin.configure { |c| c.tenancy_scope = :platform }
|
|
|
77
77
|
Seams::Admin.configure { |c| c.tenancy_scope = :tenant }
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
- **Gate:** `
|
|
81
|
-
|
|
80
|
+
- **Gate:** `Seams::Permissions.can?(role:, ability:)` — each tenant
|
|
81
|
+
policy resolves its resource's ability code against the membership
|
|
82
|
+
role read off `Accounts::Current.membership` (no `role == "admin"`
|
|
83
|
+
literal). Platform `staff?` and the `system` role still bypass.
|
|
82
84
|
- **Visibility:** only the current Account's data — every Pundit
|
|
83
85
|
`Scope` filters by `account_id`.
|
|
84
86
|
- **Use case:** a tenant-facing admin surface where each customer's
|
|
@@ -87,6 +89,34 @@ Seams::Admin.configure { |c| c.tenancy_scope = :tenant }
|
|
|
87
89
|
- **Requires:** the `accounts` engine. Without it the admin engine
|
|
88
90
|
fails fast at boot with a clear error message.
|
|
89
91
|
|
|
92
|
+
Tenant mode needs two more settings in the same initializer. With only
|
|
93
|
+
`tenancy_scope = :tenant`, a tenant admin who is not platform staff gets
|
|
94
|
+
a 403 from the gate, and no membership is resolved for admin requests:
|
|
95
|
+
|
|
96
|
+
1. **Authenticator.** The default gate is `current_identity&.staff?`,
|
|
97
|
+
which only lets platform staff in. Let signed-in identities through
|
|
98
|
+
and leave per-record decisions to the tenant policies.
|
|
99
|
+
2. **Membership resolver.** The admin controllers do not inherit your
|
|
100
|
+
`ApplicationController`, so whatever sets `Accounts::Current.membership`
|
|
101
|
+
there does not run for admin requests. Resolve it here.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# config/initializers/seams_admin.rb
|
|
105
|
+
Seams::Admin.configure do |c|
|
|
106
|
+
c.tenancy_scope = :tenant
|
|
107
|
+
c.authenticator = ->(ctrl) { ctrl.current_identity.present? }
|
|
108
|
+
# Example: the identity's first active membership. Pick the account the
|
|
109
|
+
# way your app does (subdomain, session, a switcher).
|
|
110
|
+
c.current_membership_resolver = lambda do |ctrl|
|
|
111
|
+
identity = ctrl.current_identity
|
|
112
|
+
identity && Accounts::Membership.find_by(identity_id: identity.id, active: true)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
With both set, a member-role identity still gets 403 from the policies,
|
|
118
|
+
and an admin sees only its own account's rows.
|
|
119
|
+
|
|
90
120
|
Switching modes is a config flip — no schema change, no regenerate.
|
|
91
121
|
The `policy_namespace` lookup on `Seams::Admin::ApplicationController`
|
|
92
122
|
reads `Seams::Admin.config.tenancy_scope` on every request.
|
|
@@ -170,7 +200,7 @@ bin/rails generate administrate:dashboard MyHost::Project
|
|
|
170
200
|
# Move app/dashboards/my_host/project_dashboard.rb to
|
|
171
201
|
# engines/admin/app/dashboards/admin/project_dashboard.rb
|
|
172
202
|
# Move the controller similarly under app/controllers/admin/.
|
|
173
|
-
# Add `resources :projects, controller: "admin/projects"` to
|
|
203
|
+
# Add `resources :projects, controller: "/admin/projects"` to
|
|
174
204
|
# engines/admin/config/routes.rb (or splice via the
|
|
175
205
|
# admin.routes.after_resources insertion point in a follow-up generator).
|
|
176
206
|
```
|
data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt
CHANGED
|
@@ -41,7 +41,67 @@ module Seams
|
|
|
41
41
|
# policy; a request that fails a policy never reaches the action;
|
|
42
42
|
# the audit row only writes after a successful action.
|
|
43
43
|
class ApplicationController < ::Administrate::ApplicationController
|
|
44
|
+
# Administrate's base controller does not inherit the host's
|
|
45
|
+
# ApplicationController, so it never picks up the auth engine's
|
|
46
|
+
# session lookup. Include it here, before the gate, so
|
|
47
|
+
# `current_identity` resolves and `Auth::Current.identity` is set
|
|
48
|
+
# for pundit_user and the audit actor.
|
|
49
|
+
include ::Auth::Authentication if defined?(::Auth::Authentication)
|
|
44
50
|
include ::Seams::Admin::Authenticator
|
|
51
|
+
|
|
52
|
+
# The dashboard controllers are top-level `Admin::*`, outside the
|
|
53
|
+
# isolated Seams::Admin namespace, so Rails wires them to the host
|
|
54
|
+
# app's routes. Administrate's views call url_for against the
|
|
55
|
+
# current controller's routes, so give each subclass the engine's
|
|
56
|
+
# route set instead (the later include wins).
|
|
57
|
+
def self.inherited(subclass)
|
|
58
|
+
super
|
|
59
|
+
subclass.include(::Seams::Admin.railtie_routes_url_helpers)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# [route_key, action] pairs for this engine's dashboard routes.
|
|
63
|
+
def self.admin_routes
|
|
64
|
+
::Seams::Admin::Engine.routes.routes.filter_map do |route|
|
|
65
|
+
controller = route.defaults[:controller]
|
|
66
|
+
next unless controller&.start_with?("admin/")
|
|
67
|
+
|
|
68
|
+
[controller.delete_prefix("admin/"), route.defaults[:action]]
|
|
69
|
+
end.to_set
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# "Auth::Identity" => "identities", derived from each dashboard's
|
|
73
|
+
# declared model so follow-up dashboards are picked up too.
|
|
74
|
+
def self.admin_route_keys_by_model
|
|
75
|
+
admin_routes.map(&:first).uniq.each_with_object({}) do |key, map|
|
|
76
|
+
dashboard = "Admin::#{key.classify}Dashboard".safe_constantize
|
|
77
|
+
map[dashboard.model.name] = key if dashboard.respond_to?(:model)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Accounts::Membership and Teams::Membership both have the route
|
|
82
|
+
# key `memberships` (isolated-engine model naming), so
|
|
83
|
+
# Administrate's polymorphic `[:admin, membership]` calls
|
|
84
|
+
# admin_membership(s)_path, which does not exist. These helpers
|
|
85
|
+
# route by the record's class, or by the current controller for a
|
|
86
|
+
# new record's form URL.
|
|
87
|
+
MEMBERSHIP_ROUTE_KEYS = {
|
|
88
|
+
"Accounts::Membership" => "accounts_membership",
|
|
89
|
+
"Teams::Membership" => "teams_membership"
|
|
90
|
+
}.freeze
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
"admin_memberships" => "admin_%ss",
|
|
94
|
+
"admin_membership" => "admin_%s",
|
|
95
|
+
"new_admin_membership" => "new_admin_%s",
|
|
96
|
+
"edit_admin_membership" => "edit_admin_%s"
|
|
97
|
+
}.each do |name, target|
|
|
98
|
+
%w[path url].each do |suffix|
|
|
99
|
+
define_method(:"#{name}_#{suffix}") do |*args, **opts|
|
|
100
|
+
public_send(:"#{format(target, membership_route_key(args.first))}_#{suffix}", *args, **opts)
|
|
101
|
+
end
|
|
102
|
+
helper_method :"#{name}_#{suffix}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
45
105
|
# Administrate's canonical Pundit integration. Internally:
|
|
46
106
|
# - includes Pundit::Authorization
|
|
47
107
|
# - overrides `authorized_action?` to call
|
|
@@ -78,10 +138,12 @@ module Seams
|
|
|
78
138
|
# `authorize` / `policy_scope`. Administrate's `Punditize`
|
|
79
139
|
# already calls them on every standard action, so any future
|
|
80
140
|
# custom action added to a subclass that forgets the call gets
|
|
81
|
-
# caught loudly in development. `:
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
141
|
+
# caught loudly in development. `:except` skips `:index`, which
|
|
142
|
+
# is covered by `verify_policy_scoped` instead. The 403 rescue
|
|
143
|
+
# responder needs no exclusion: after_actions do not run when
|
|
144
|
+
# rescue_from handles the exception, and naming a non-action
|
|
145
|
+
# here raises ActionNotFound on Rails 7.1+.
|
|
146
|
+
after_action :verify_authorized, except: :index
|
|
85
147
|
after_action :verify_policy_scoped, only: %i[index]
|
|
86
148
|
|
|
87
149
|
# Helper exposed to dashboards so they can read the active
|
|
@@ -89,6 +151,7 @@ module Seams
|
|
|
89
151
|
# policy scopes; dashboards may also branch on it for
|
|
90
152
|
# account_id columns.
|
|
91
153
|
helper_method :seams_admin_tenancy_scope
|
|
154
|
+
helper_method :seams_admin_navigation
|
|
92
155
|
|
|
93
156
|
def seams_admin_tenancy_scope
|
|
94
157
|
::Seams::Admin.config.tenancy_scope
|
|
@@ -125,6 +188,13 @@ module Seams
|
|
|
125
188
|
# at runtime per-request (rare; typically configured once at boot)
|
|
126
189
|
# override this method on a subclass and consult their own request
|
|
127
190
|
# state.
|
|
191
|
+
# Administrate's `create` never sets `@requested_resource`, and
|
|
192
|
+
# calling `requested_resource` there looks up `params[:id]` (nil).
|
|
193
|
+
# Keep the saved record for record_admin_audit.
|
|
194
|
+
def create
|
|
195
|
+
super { |resource| @created_resource = resource }
|
|
196
|
+
end
|
|
197
|
+
|
|
128
198
|
def policy_namespace
|
|
129
199
|
case ::Seams::Admin.config.tenancy_scope
|
|
130
200
|
when :tenant then [::Admin::Tenant]
|
|
@@ -134,6 +204,82 @@ module Seams
|
|
|
134
204
|
|
|
135
205
|
private
|
|
136
206
|
|
|
207
|
+
# Pundit's array lookup cannot reach these policies on its own: it
|
|
208
|
+
# turns a Module namespace element into `Module`, and it keeps the
|
|
209
|
+
# record's namespace (`Admin::Platform::Auth::IdentityPolicy`).
|
|
210
|
+
# Each dashboard controller maps 1:1 to a policy instead
|
|
211
|
+
# (Admin::AccountsMembershipsController ->
|
|
212
|
+
# Admin::Platform::AccountsMembershipPolicy), so resolve the class
|
|
213
|
+
# explicitly and hand it to Pundit's own `policy_class:` options.
|
|
214
|
+
# That keeps verify_authorized / verify_policy_scoped working.
|
|
215
|
+
# Administrate's flash messages name the resource through a
|
|
216
|
+
# resolver that guesses the model from the controller path
|
|
217
|
+
# ("admin/plans" -> Plan). Use the dashboard's real model instead.
|
|
218
|
+
def translate_with_resource(key)
|
|
219
|
+
t("administrate.controller.#{key}", resource: resource_class.model_name.human)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def membership_route_key(record)
|
|
223
|
+
MEMBERSHIP_ROUTE_KEYS[record.class.name] || controller_name.singularize
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def admin_policy_class
|
|
227
|
+
"#{policy_namespace.first}::#{controller_name.classify}Policy".constantize
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def scoped_resource
|
|
231
|
+
policy_scope(resource_class.default_scoped, policy_scope_class: admin_policy_class::Scope)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def authorize_resource(resource)
|
|
235
|
+
authorize(resource, policy_class: admin_policy_class)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Views ask about other resources too (navigation, association
|
|
239
|
+
# lists), so pick the policy for the resource being linked, not
|
|
240
|
+
# this controller's. Unknown resources are denied.
|
|
241
|
+
def authorized_action?(resource, action)
|
|
242
|
+
key = admin_route_key_for(resource) || controller_name
|
|
243
|
+
policy = "#{policy_namespace.first}::#{key.classify}Policy".safe_constantize
|
|
244
|
+
return false if policy.nil?
|
|
245
|
+
|
|
246
|
+
policy.new(pundit_user, resource).public_send(:"#{action}?")
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Administrate decides which links to render with existing_action?,
|
|
250
|
+
# built from Rails.application.routes. A mounted engine's routes are
|
|
251
|
+
# not in that set, so every show/edit/new/destroy link and the
|
|
252
|
+
# navigation vanished. Read this engine's routes instead.
|
|
253
|
+
def existing_action?(resource, action_name)
|
|
254
|
+
key = admin_route_key_for(resource)
|
|
255
|
+
key.present? && self.class.admin_routes.include?([key, action_name.to_s])
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Route key ("accounts_memberships") for a record, a model class,
|
|
259
|
+
# or a name Administrate passes ("admin/accounts_membership",
|
|
260
|
+
# :identities).
|
|
261
|
+
def admin_route_key_for(resource)
|
|
262
|
+
case resource
|
|
263
|
+
when ::ActiveRecord::Base then self.class.admin_route_keys_by_model[resource.class.name]
|
|
264
|
+
when Class then self.class.admin_route_keys_by_model[resource.name]
|
|
265
|
+
else resource.to_s.delete_prefix("admin/").pluralize.presence
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Sidebar entries for the navigation partial: every dashboard with
|
|
270
|
+
# an index route that the current user may list.
|
|
271
|
+
def seams_admin_navigation
|
|
272
|
+
self.class.admin_routes.select { |_key, action| action == "index" }.map(&:first).uniq.filter_map do |key|
|
|
273
|
+
dashboard = "Admin::#{key.classify}Dashboard".safe_constantize
|
|
274
|
+
next unless dashboard.respond_to?(:model) && authorized_action?(dashboard.model, :index)
|
|
275
|
+
|
|
276
|
+
{ key: key,
|
|
277
|
+
label: dashboard.model.model_name.human(count: 2),
|
|
278
|
+
path: url_for(controller: "/admin/#{key}", action: :index),
|
|
279
|
+
active: key == controller_name }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
137
283
|
# Resolve the current Accounts::Membership for tenant-mode
|
|
138
284
|
# policy decisions. Delegates to
|
|
139
285
|
# `Seams::Admin.config.current_membership_resolver` (a callable
|
|
@@ -170,10 +316,10 @@ module Seams
|
|
|
170
316
|
# row from this controller would double-log. We detect the
|
|
171
317
|
# `audit_logs` association the concern installs and skip.
|
|
172
318
|
#
|
|
173
|
-
# `
|
|
174
|
-
#
|
|
175
|
-
# just-updated record; on `destroy`
|
|
176
|
-
# destroyed (frozen but still readable for `id` / `class.name`).
|
|
319
|
+
# The audited record: on `create` the record our `create` override
|
|
320
|
+
# captured (Administrate never sets requested_resource there); on
|
|
321
|
+
# `update` the just-updated record; on `destroy` the record that was
|
|
322
|
+
# just destroyed (frozen but still readable for `id` / `class.name`).
|
|
177
323
|
def record_admin_audit
|
|
178
324
|
return unless defined?(::Core::AuditLog)
|
|
179
325
|
return unless response.successful? || response.redirect?
|
|
@@ -215,6 +361,8 @@ module Seams
|
|
|
215
361
|
# subclass that overrode `requested_resource` to raise) we don't
|
|
216
362
|
# want the audit hook to take down the whole response.
|
|
217
363
|
def requested_resource_for_audit
|
|
364
|
+
return @created_resource if action_name == "create"
|
|
365
|
+
|
|
218
366
|
respond_to?(:requested_resource, true) ? requested_resource : nil
|
|
219
367
|
rescue StandardError
|
|
220
368
|
nil
|
|
@@ -11,6 +11,12 @@ require "administrate/base_dashboard"
|
|
|
11
11
|
# secondary identifier and renders fine as Field::Number.
|
|
12
12
|
module Admin
|
|
13
13
|
class AccountDashboard < Administrate::BaseDashboard
|
|
14
|
+
# Administrate derives the model from the class name
|
|
15
|
+
# (AccountDashboard -> Admin::Account), which does not exist.
|
|
16
|
+
def self.model
|
|
17
|
+
::Accounts::Account
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
ATTRIBUTE_TYPES = {
|
|
15
21
|
# UUID PK — Field::String, not Field::Number.
|
|
16
22
|
id: Field::String,
|
|
@@ -18,7 +24,9 @@ module Admin
|
|
|
18
24
|
external_account_id: Field::Number,
|
|
19
25
|
cancelled_at: Field::DateTime,
|
|
20
26
|
incinerated_at: Field::DateTime,
|
|
21
|
-
memberships:
|
|
27
|
+
memberships: Admin::Fields::HasMany.with_options(
|
|
28
|
+
class_name: "Accounts::Membership", dashboard: "Admin::AccountsMembershipDashboard"
|
|
29
|
+
),
|
|
22
30
|
created_at: Field::DateTime,
|
|
23
31
|
updated_at: Field::DateTime
|
|
24
32
|
}.freeze
|
data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt
CHANGED
|
@@ -17,10 +17,18 @@ require "administrate/base_dashboard"
|
|
|
17
17
|
# the admin surface treats identity_id as a typed integer reference.
|
|
18
18
|
module Admin
|
|
19
19
|
class AccountsMembershipDashboard < Administrate::BaseDashboard
|
|
20
|
+
# Administrate derives the model from the class name
|
|
21
|
+
# (AccountsMembershipDashboard -> Admin::AccountsMembership), which does not exist.
|
|
22
|
+
def self.model
|
|
23
|
+
::Accounts::Membership
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
ATTRIBUTE_TYPES = {
|
|
21
27
|
# UUID PK — Field::String, not Field::Number.
|
|
22
28
|
id: Field::String,
|
|
23
|
-
account:
|
|
29
|
+
account: Admin::Fields::BelongsTo.with_options(
|
|
30
|
+
class_name: "Accounts::Account", dashboard: "Admin::AccountDashboard"
|
|
31
|
+
),
|
|
24
32
|
identity_id: Field::Number,
|
|
25
33
|
name: Field::String,
|
|
26
34
|
role: Field::Select.with_options(collection: %w[owner admin member system]),
|
|
@@ -13,6 +13,12 @@ require "administrate/base_dashboard"
|
|
|
13
13
|
# auth engine's reset flow, not through the admin surface.
|
|
14
14
|
module Admin
|
|
15
15
|
class IdentityDashboard < Administrate::BaseDashboard
|
|
16
|
+
# Administrate derives the model from the class name
|
|
17
|
+
# (IdentityDashboard -> Admin::Identity), which does not exist.
|
|
18
|
+
def self.model
|
|
19
|
+
::Auth::Identity
|
|
20
|
+
end
|
|
21
|
+
|
|
16
22
|
# ATTRIBUTE_TYPES describes how Administrate renders each column.
|
|
17
23
|
# Administrate auto-discovers attributes from the model; declaring
|
|
18
24
|
# them explicitly here makes the rendering deterministic and lets
|
|
@@ -8,9 +8,15 @@ require "administrate/base_dashboard"
|
|
|
8
8
|
# read the full token (they re-issue if lost).
|
|
9
9
|
module Admin
|
|
10
10
|
class InvitationDashboard < Administrate::BaseDashboard
|
|
11
|
+
# Administrate derives the model from the class name
|
|
12
|
+
# (InvitationDashboard -> Admin::Invitation), which does not exist.
|
|
13
|
+
def self.model
|
|
14
|
+
::Teams::Invitation
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
ATTRIBUTE_TYPES = {
|
|
12
18
|
id: Field::Number,
|
|
13
|
-
team:
|
|
19
|
+
team: Admin::Fields::BelongsTo.with_options(class_name: "Teams::Team", dashboard: "Admin::TeamDashboard"),
|
|
14
20
|
email: Field::String.with_options(searchable: true),
|
|
15
21
|
# Truncated to 16 chars so the table view doesn't get blown up
|
|
16
22
|
# by the 43-char base64 token. Full value still available on the
|
|
@@ -12,6 +12,12 @@ require "administrate/base_dashboard"
|
|
|
12
12
|
# column on the same row.
|
|
13
13
|
module Admin
|
|
14
14
|
class InvoiceDashboard < Administrate::BaseDashboard
|
|
15
|
+
# Administrate derives the model from the class name
|
|
16
|
+
# (InvoiceDashboard -> Admin::Invoice), which does not exist.
|
|
17
|
+
def self.model
|
|
18
|
+
::Billing::Invoice
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
ATTRIBUTE_TYPES = {
|
|
16
22
|
id: Field::Number,
|
|
17
23
|
# UUID — Field::String, not Field::Number.
|
data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt
CHANGED
|
@@ -8,6 +8,12 @@ require "administrate/base_dashboard"
|
|
|
8
8
|
# track the human (Auth::Identity bigint) who pressed the button.
|
|
9
9
|
module Admin
|
|
10
10
|
class LifetimePassDashboard < Administrate::BaseDashboard
|
|
11
|
+
# Administrate derives the model from the class name
|
|
12
|
+
# (LifetimePassDashboard -> Admin::LifetimePass), which does not exist.
|
|
13
|
+
def self.model
|
|
14
|
+
::Billing::LifetimePass
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
ATTRIBUTE_TYPES = {
|
|
12
18
|
id: Field::Number,
|
|
13
19
|
# UUID — Field::String, not Field::Number.
|
|
@@ -12,6 +12,12 @@ require "administrate/base_dashboard"
|
|
|
12
12
|
# collection view and only shown on the show page.
|
|
13
13
|
module Admin
|
|
14
14
|
class NotificationDashboard < Administrate::BaseDashboard
|
|
15
|
+
# Administrate derives the model from the class name
|
|
16
|
+
# (NotificationDashboard -> Admin::Notification), which does not exist.
|
|
17
|
+
def self.model
|
|
18
|
+
::Notifications::Notification
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
ATTRIBUTE_TYPES = {
|
|
16
22
|
id: Field::Number,
|
|
17
23
|
type: Field::String,
|
|
@@ -8,6 +8,12 @@ require "administrate/base_dashboard"
|
|
|
8
8
|
# false` is an explicit opt-out.
|
|
9
9
|
module Admin
|
|
10
10
|
class NotificationPreferenceDashboard < Administrate::BaseDashboard
|
|
11
|
+
# Administrate derives the model from the class name
|
|
12
|
+
# (NotificationPreferenceDashboard -> Admin::NotificationPreference), which does not exist.
|
|
13
|
+
def self.model
|
|
14
|
+
::Notifications::NotificationPreference
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
ATTRIBUTE_TYPES = {
|
|
12
18
|
id: Field::Number,
|
|
13
19
|
identity_id: Field::Number,
|
|
@@ -12,6 +12,12 @@ require "administrate/base_dashboard"
|
|
|
12
12
|
# `currency` from the same row and formats accordingly.
|
|
13
13
|
module Admin
|
|
14
14
|
class PlanDashboard < Administrate::BaseDashboard
|
|
15
|
+
# Administrate derives the model from the class name
|
|
16
|
+
# (PlanDashboard -> Admin::Plan), which does not exist.
|
|
17
|
+
def self.model
|
|
18
|
+
::Billing::Plan
|
|
19
|
+
end
|
|
20
|
+
|
|
15
21
|
ATTRIBUTE_TYPES = {
|
|
16
22
|
id: Field::Number,
|
|
17
23
|
gateway_ref: Field::String,
|
|
@@ -10,6 +10,12 @@ require "administrate/base_dashboard"
|
|
|
10
10
|
# account_id as an opaque tenant reference rather than a belongs_to.
|
|
11
11
|
module Admin
|
|
12
12
|
class SubscriptionDashboard < Administrate::BaseDashboard
|
|
13
|
+
# Administrate derives the model from the class name
|
|
14
|
+
# (SubscriptionDashboard -> Admin::Subscription), which does not exist.
|
|
15
|
+
def self.model
|
|
16
|
+
::Billing::Subscription
|
|
17
|
+
end
|
|
18
|
+
|
|
13
19
|
ATTRIBUTE_TYPES = {
|
|
14
20
|
id: Field::Number,
|
|
15
21
|
# UUID — Field::String, not Field::Number.
|
|
@@ -8,12 +8,22 @@ require "administrate/base_dashboard"
|
|
|
8
8
|
# (Teams::Membership), not via Accounts::Membership.
|
|
9
9
|
module Admin
|
|
10
10
|
class TeamDashboard < Administrate::BaseDashboard
|
|
11
|
+
# Administrate derives the model from the class name
|
|
12
|
+
# (TeamDashboard -> Admin::Team), which does not exist.
|
|
13
|
+
def self.model
|
|
14
|
+
::Teams::Team
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
ATTRIBUTE_TYPES = {
|
|
12
18
|
id: Field::Number,
|
|
13
19
|
name: Field::String,
|
|
14
20
|
slug: Field::String,
|
|
15
|
-
memberships:
|
|
16
|
-
|
|
21
|
+
memberships: Admin::Fields::HasMany.with_options(
|
|
22
|
+
class_name: "Teams::Membership", dashboard: "Admin::TeamsMembershipDashboard"
|
|
23
|
+
),
|
|
24
|
+
invitations: Admin::Fields::HasMany.with_options(
|
|
25
|
+
class_name: "Teams::Invitation", dashboard: "Admin::InvitationDashboard"
|
|
26
|
+
),
|
|
17
27
|
created_at: Field::DateTime,
|
|
18
28
|
updated_at: Field::DateTime
|
|
19
29
|
}.freeze
|
data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt
CHANGED
|
@@ -14,9 +14,15 @@ require "administrate/base_dashboard"
|
|
|
14
14
|
# treats it as a typed integer reference rather than a belongs_to.
|
|
15
15
|
module Admin
|
|
16
16
|
class TeamsMembershipDashboard < Administrate::BaseDashboard
|
|
17
|
+
# Administrate derives the model from the class name
|
|
18
|
+
# (TeamsMembershipDashboard -> Admin::TeamsMembership), which does not exist.
|
|
19
|
+
def self.model
|
|
20
|
+
::Teams::Membership
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
ATTRIBUTE_TYPES = {
|
|
18
24
|
id: Field::Number,
|
|
19
|
-
team:
|
|
25
|
+
team: Admin::Fields::BelongsTo.with_options(class_name: "Teams::Team", dashboard: "Admin::TeamDashboard"),
|
|
20
26
|
identity_id: Field::Number,
|
|
21
27
|
role: Field::Select.with_options(collection: %w[owner admin member]),
|
|
22
28
|
created_at: Field::DateTime,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
module Fields
|
|
5
|
+
# Administrate::Field::BelongsTo plus the `dashboard:` option (see
|
|
6
|
+
# Admin::Fields::DashboardOption). Renders with the stock
|
|
7
|
+
# fields/belongs_to partials.
|
|
8
|
+
class BelongsTo < ::Administrate::Field::BelongsTo
|
|
9
|
+
include DashboardOption
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
module Fields
|
|
5
|
+
# Administrate finds an association's dashboard by appending
|
|
6
|
+
# "Dashboard" to the associated class name (Teams::Team ->
|
|
7
|
+
# Teams::TeamDashboard). This engine's dashboards live under Admin::,
|
|
8
|
+
# and defining constants inside another engine's namespace would
|
|
9
|
+
# break the engine boundary. The `dashboard:` option names the
|
|
10
|
+
# dashboard explicitly:
|
|
11
|
+
#
|
|
12
|
+
# team: Admin::Fields::BelongsTo.with_options(
|
|
13
|
+
# class_name: "Teams::Team", dashboard: "Admin::TeamDashboard"
|
|
14
|
+
# )
|
|
15
|
+
module DashboardOption
|
|
16
|
+
def associated_dashboard
|
|
17
|
+
name = options[:dashboard]
|
|
18
|
+
name ? name.constantize.new : super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Admin
|
|
4
|
+
module Fields
|
|
5
|
+
# Administrate::Field::HasMany plus the `dashboard:` option (see
|
|
6
|
+
# Admin::Fields::DashboardOption). Renders with the stock
|
|
7
|
+
# fields/has_many partials.
|
|
8
|
+
class HasMany < ::Administrate::Field::HasMany
|
|
9
|
+
include DashboardOption
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt
CHANGED
|
@@ -3,10 +3,24 @@
|
|
|
3
3
|
module Admin
|
|
4
4
|
module Tenant
|
|
5
5
|
# Abstract base for every tenant-mode admin policy. Tenant mode
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# resolves access through `Seams::Permissions.can?` — the gem's
|
|
7
|
+
# single source of truth for authorisation — keyed by the
|
|
8
|
+
# currently-resolved membership's role
|
|
9
|
+
# (`Seams::Admin::Context#membership.role`) and the ability code
|
|
10
|
+
# the concrete policy owns (see `permission_ability`). There is
|
|
11
|
+
# deliberately no `role == "admin"` literal here: the
|
|
12
|
+
# owner/admin/member hierarchy and the grant map live in
|
|
13
|
+
# `Seams::Permissions::ROLE_HIERARCHY` and
|
|
14
|
+
# `Seams.configuration.permission_grants`, so the admin surface and
|
|
15
|
+
# the host controllers decide from the same map and never drift.
|
|
16
|
+
#
|
|
17
|
+
# Two bypass tiers remain explicit and intentional, mirroring
|
|
18
|
+
# `Accounts::Authorization#authorize_permission!`:
|
|
19
|
+
# - Platform staff (`Auth::Identity#staff?`) always pass — a
|
|
20
|
+
# platform staff member can drop into any tenant's admin without
|
|
21
|
+
# rejoining as an account-admin.
|
|
22
|
+
# - The `system` pseudo-role bypasses inside
|
|
23
|
+
# `Seams::Permissions.can?` itself (trusted internal callers).
|
|
10
24
|
#
|
|
11
25
|
# The `Scope` inner class is overridden per-policy; this base
|
|
12
26
|
# returns `scope.none` so a subclass that forgets to override gets
|
|
@@ -118,16 +132,43 @@ module Admin
|
|
|
118
132
|
|
|
119
133
|
private
|
|
120
134
|
|
|
121
|
-
# Permitted when (a) the Identity is staff (
|
|
122
|
-
# into every tenant), or (b) the
|
|
123
|
-
#
|
|
135
|
+
# Permitted when (a) the Identity is platform staff (the bypass
|
|
136
|
+
# tier — platform admins drop into every tenant), or (b) the
|
|
137
|
+
# resolved Accounts::Membership's role holds this policy's ability
|
|
138
|
+
# in the gem's permission registry. Resolution is delegated to
|
|
139
|
+
# `Seams::Permissions.can?` so the owner/admin/member hierarchy
|
|
140
|
+
# and the host grant map are the only place access is decided.
|
|
124
141
|
def permitted?
|
|
142
|
+
permitted_to?(permission_ability)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Shared resolution path for every tenant policy: apply the staff
|
|
146
|
+
# bypass, then defer to `Seams::Permissions.can?` with the
|
|
147
|
+
# membership role and the given ability code. Concrete policies
|
|
148
|
+
# never re-implement the role logic — they only name their
|
|
149
|
+
# ability via `permission_ability`.
|
|
150
|
+
#
|
|
151
|
+
# Fail closed: a request with no resolved membership, or a policy
|
|
152
|
+
# that names no ability, is denied (only platform staff pass).
|
|
153
|
+
def permitted_to?(ability)
|
|
125
154
|
return true if user&.identity&.staff?
|
|
155
|
+
return false if ability.nil?
|
|
156
|
+
|
|
157
|
+
role = user&.membership&.role
|
|
158
|
+
return false if role.nil?
|
|
126
159
|
|
|
127
|
-
|
|
128
|
-
|
|
160
|
+
::Seams::Permissions.can?(role: role.to_s, ability: ability)
|
|
161
|
+
end
|
|
129
162
|
|
|
130
|
-
|
|
163
|
+
# The owning engine's ability code (resource.action.engine) this
|
|
164
|
+
# policy resolves through. Each concrete tenant policy overrides
|
|
165
|
+
# this to name its resource's `manage` ability — the code the
|
|
166
|
+
# owning engine registers at boot and that
|
|
167
|
+
# `Seams::Permissions::DEFAULT_GRANTS` grants to the admin tier.
|
|
168
|
+
# nil here fails closed (only platform staff pass), so a subclass
|
|
169
|
+
# that forgets to name its ability never silently grants access.
|
|
170
|
+
def permission_ability
|
|
171
|
+
nil
|
|
131
172
|
end
|
|
132
173
|
|
|
133
174
|
def account_id
|