seams 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 +335 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
- data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
- data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
- data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
- data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
- data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
- data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
- data/lib/generators/seams/admin/admin_generator.rb +852 -0
- data/lib/generators/seams/admin/templates/README.md.tt +266 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
- data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
- data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
- data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
- data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
- data/lib/generators/seams/auth/auth_generator.rb +311 -0
- data/lib/generators/seams/auth/templates/README.md.tt +289 -0
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
- data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
- data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
- data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
- data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
- data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
- data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
- data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
- data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
- data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
- data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
- data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
- data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
- data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
- data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
- data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
- data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
- data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
- data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
- data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
- data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
- data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
- data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
- data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
- data/lib/generators/seams/billing/billing_generator.rb +476 -0
- data/lib/generators/seams/billing/templates/README.md.tt +355 -0
- data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
- data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
- data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
- data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
- data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
- data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
- data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
- data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
- data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
- data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
- data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
- data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
- data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
- data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
- data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
- data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
- data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
- data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
- data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
- data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
- data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
- data/lib/generators/seams/core/core_generator.rb +191 -0
- data/lib/generators/seams/core/templates/README.md.tt +45 -0
- data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
- data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
- data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
- data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
- data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
- data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
- data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
- data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
- data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
- data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
- data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
- data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
- data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
- data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
- data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
- data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
- data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
- data/lib/generators/seams/engine/engine_generator.rb +165 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
- data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
- data/lib/generators/seams/engine/templates/README.md.tt +40 -0
- data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
- data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
- data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
- data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
- data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
- data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
- data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
- data/lib/generators/seams/install/install_generator.rb +211 -0
- data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
- data/lib/generators/seams/install/templates/Procfile.tt +14 -0
- data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
- data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
- data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
- data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
- data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
- data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
- data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
- data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
- data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
- data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
- data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
- data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
- data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
- data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
- data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
- data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
- data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
- data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
- data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
- data/lib/generators/seams/remove/remove_generator.rb +259 -0
- data/lib/generators/seams/teams/teams_generator.rb +298 -0
- data/lib/generators/seams/teams/templates/README.md.tt +88 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
- data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
- data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
- data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
- data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
- data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
- data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
- data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
- data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
- data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
- data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
- data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
- data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
- data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
- data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
- data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
- data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
- data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
- data/lib/seams/cli/list.rb +111 -0
- data/lib/seams/cli/quality.rb +99 -0
- data/lib/seams/cli/resolve.rb +276 -0
- data/lib/seams/cli/test_changed.rb +116 -0
- data/lib/seams/cli.rb +48 -0
- data/lib/seams/configuration.rb +19 -0
- data/lib/seams/cops/known_queue_names.rb +42 -0
- data/lib/seams/cops/migration_comments.rb +68 -0
- data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
- data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
- data/lib/seams/cops.rb +18 -0
- data/lib/seams/event_registry.rb +49 -0
- data/lib/seams/events/adapter.rb +24 -0
- data/lib/seams/events/adapters/active_support.rb +31 -0
- data/lib/seams/events/publisher.rb +178 -0
- data/lib/seams/events.rb +39 -0
- data/lib/seams/generators/dummy_app_writer.rb +424 -0
- data/lib/seams/generators/eject_aware.rb +102 -0
- data/lib/seams/generators/follow_up_generator.rb +148 -0
- data/lib/seams/generators/host_injector.rb +124 -0
- data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
- data/lib/seams/generators/splicer.rb +217 -0
- data/lib/seams/observability/adapter.rb +33 -0
- data/lib/seams/observability/adapters/rails_logger.rb +59 -0
- data/lib/seams/observability.rb +34 -0
- data/lib/seams/runtime.rb +23 -0
- data/lib/seams/version.rb +5 -0
- data/lib/seams.rb +23 -0
- metadata +493 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Accounts
|
|
4
|
+
# Joins an Auth::Identity to an Accounts::Account with a role.
|
|
5
|
+
#
|
|
6
|
+
# `identity_id` is **nullable**: when set, the row represents a real
|
|
7
|
+
# human in this Account (an owner, admin, or member). When NULL, the
|
|
8
|
+
# row is a system actor — the audit-log writer for changes that
|
|
9
|
+
# don't have a human behind them (background jobs, webhook
|
|
10
|
+
# ingestion, scheduled tasks). Every Account ships with exactly one
|
|
11
|
+
# `role: :system, identity_id: nil` row, created by
|
|
12
|
+
# `Account.create_with_owner`.
|
|
13
|
+
#
|
|
14
|
+
# An Identity can have at most one Membership per Account
|
|
15
|
+
# (enforced by the unique compound index in the migration); the
|
|
16
|
+
# role lives on the Membership, not the Identity.
|
|
17
|
+
class Membership < ApplicationRecord
|
|
18
|
+
self.table_name = "accounts_memberships"
|
|
19
|
+
self.primary_key = "id"
|
|
20
|
+
self.implicit_order_column = "created_at"
|
|
21
|
+
|
|
22
|
+
ROLES = %w[owner admin member system].freeze
|
|
23
|
+
|
|
24
|
+
belongs_to :account, class_name: "Accounts::Account"
|
|
25
|
+
|
|
26
|
+
validates :name, presence: true
|
|
27
|
+
validates :role, inclusion: { in: ROLES }
|
|
28
|
+
validates :identity_id,
|
|
29
|
+
uniqueness: { scope: :account_id, allow_nil: true,
|
|
30
|
+
message: "already has a membership in this account" }
|
|
31
|
+
|
|
32
|
+
scope :owner, -> { where(role: "owner") }
|
|
33
|
+
scope :admin, -> { where(role: %w[owner admin]) }
|
|
34
|
+
scope :member, -> { where(role: "member") }
|
|
35
|
+
# `:active` excludes the system actor — callers that want every
|
|
36
|
+
# actor (including system) should query without the scope.
|
|
37
|
+
scope :active, -> { where(active: true).where.not(role: "system") }
|
|
38
|
+
scope :system_actors, -> { where(role: "system") }
|
|
39
|
+
|
|
40
|
+
after_create_commit :publish_membership_created
|
|
41
|
+
after_update_commit :publish_role_changed, if: :saved_change_to_role?
|
|
42
|
+
after_destroy_commit :publish_membership_removed
|
|
43
|
+
|
|
44
|
+
def owner?
|
|
45
|
+
role == "owner"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def admin?
|
|
49
|
+
%w[owner admin].include?(role)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def system?
|
|
53
|
+
role == "system"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Can `self` perform admin-level changes against `other` (another
|
|
57
|
+
# Membership)? Owners can change anyone, admins can change anyone
|
|
58
|
+
# except an owner, members and the system actor cannot change
|
|
59
|
+
# anyone. Pairs with `can_change?` for the slightly more
|
|
60
|
+
# permissive change-anything-non-destructive rule.
|
|
61
|
+
def can_administer?(other)
|
|
62
|
+
return false if system?
|
|
63
|
+
return false unless admin?
|
|
64
|
+
return true if owner?
|
|
65
|
+
|
|
66
|
+
!other.owner?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Like `can_administer?` but allows admins to change other admins
|
|
70
|
+
# (just not owners). Used for invite/remove flows where two
|
|
71
|
+
# admins should be able to manage each other.
|
|
72
|
+
def can_change?(other)
|
|
73
|
+
return false if system?
|
|
74
|
+
return false unless admin?
|
|
75
|
+
return true if owner?
|
|
76
|
+
|
|
77
|
+
!other.owner?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def publish_membership_created
|
|
83
|
+
Seams::Events::Publisher.publish(
|
|
84
|
+
"membership.created.accounts",
|
|
85
|
+
account_id: account_id,
|
|
86
|
+
membership_id: id,
|
|
87
|
+
identity_id: identity_id,
|
|
88
|
+
role: role
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def publish_role_changed
|
|
93
|
+
from_role, to_role = saved_change_to_role
|
|
94
|
+
Seams::Events::Publisher.publish(
|
|
95
|
+
"membership.role_changed.accounts",
|
|
96
|
+
account_id: account_id,
|
|
97
|
+
membership_id: id,
|
|
98
|
+
from_role: from_role,
|
|
99
|
+
to_role: to_role,
|
|
100
|
+
changed_by_identity_id: Accounts::Current.membership&.identity_id
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def publish_membership_removed
|
|
105
|
+
Seams::Events::Publisher.publish(
|
|
106
|
+
"membership.removed.accounts",
|
|
107
|
+
account_id: account_id,
|
|
108
|
+
membership_id: id,
|
|
109
|
+
identity_id: identity_id,
|
|
110
|
+
removed_by_identity_id: Accounts::Current.membership&.identity_id
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Intentionally empty. The Accounts engine ships no controllers in
|
|
4
|
+
# Wave 9 — hosts wire their own account-creation flows; the engine
|
|
5
|
+
# provides models + concerns. Future waves may add a thin controller
|
|
6
|
+
# surface.
|
|
7
|
+
Accounts::Engine.routes.draw do
|
|
8
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What: creates the accounts table for the Accounts engine.
|
|
4
|
+
# Why: Accounts::Account is the tenant boundary. Every other engine
|
|
5
|
+
# that scopes its data to a tenant binds to this row.
|
|
6
|
+
# Risk: empty table on creation — no data migration needed.
|
|
7
|
+
#
|
|
8
|
+
# UUID primary key (not bigint): account ids appear in shareable URLs
|
|
9
|
+
# and we don't want to leak row counts. external_account_id is a
|
|
10
|
+
# bigint kept alongside for slug encoding so URL slugs stay short.
|
|
11
|
+
class CreateAccounts < ActiveRecord::Migration[7.1]
|
|
12
|
+
def change
|
|
13
|
+
enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
|
|
14
|
+
|
|
15
|
+
create_table :accounts, id: :uuid do |t|
|
|
16
|
+
t.string :name, null: false
|
|
17
|
+
t.bigint :external_account_id, null: false
|
|
18
|
+
# Soft cancel: account is unusable but data is still recoverable.
|
|
19
|
+
t.datetime :cancelled_at
|
|
20
|
+
# Hard delete grace marker: when set, the host's incinerator job
|
|
21
|
+
# permanently destroys the row + cascade.
|
|
22
|
+
t.datetime :incinerated_at
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
add_index :accounts, :external_account_id, unique: true
|
|
27
|
+
add_index :accounts, :cancelled_at
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What: creates the accounts_memberships join table.
|
|
4
|
+
# Why: joins Auth::Identity to Accounts::Account with a role.
|
|
5
|
+
# identity_id is NULLABLE: rows with NULL identity_id are
|
|
6
|
+
# system actors used for audit-log writes that don't have a
|
|
7
|
+
# human behind them (background jobs, webhook ingestion).
|
|
8
|
+
# Risk: append-mostly. Unique compound index on (account_id,
|
|
9
|
+
# identity_id) ensures an Identity has at most one Membership
|
|
10
|
+
# per Account; system rows (NULL identity_id) bypass that
|
|
11
|
+
# constraint by virtue of the NULL.
|
|
12
|
+
class CreateAccountsMemberships < ActiveRecord::Migration[7.1]
|
|
13
|
+
def change
|
|
14
|
+
enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
|
|
15
|
+
|
|
16
|
+
create_table :accounts_memberships, id: :uuid do |t|
|
|
17
|
+
t.references :account, type: :uuid, null: false,
|
|
18
|
+
foreign_key: { to_table: :accounts }, index: false
|
|
19
|
+
# NULL = system actor. Otherwise references Auth::Identity. No
|
|
20
|
+
# FK to auth_identities because the auth and accounts engines
|
|
21
|
+
# may live in different schemas / databases in production —
|
|
22
|
+
# cross-engine integrity is enforced at the application layer.
|
|
23
|
+
# bigint to match auth_identities' default integer PK (the same
|
|
24
|
+
# convention every other engine — billing, teams — follows).
|
|
25
|
+
t.bigint :identity_id, null: true
|
|
26
|
+
# Denormalised display name so the Identity (and its email)
|
|
27
|
+
# can be soft-deleted / anonymised without breaking the
|
|
28
|
+
# account's audit trail.
|
|
29
|
+
t.string :name, null: false
|
|
30
|
+
t.string :role, null: false, default: "member"
|
|
31
|
+
t.boolean :active, null: false, default: true
|
|
32
|
+
t.datetime :verified_at
|
|
33
|
+
t.timestamps
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
add_index :accounts_memberships, %i[account_id identity_id], unique: true,
|
|
37
|
+
name: "index_accounts_memberships_unique"
|
|
38
|
+
add_index :accounts_memberships, %i[account_id role]
|
|
39
|
+
add_index :accounts_memberships, :identity_id
|
|
40
|
+
# Postgres treats NULLs as distinct in unique indexes, so the
|
|
41
|
+
# compound index above does NOT prevent two `(account_id, NULL)`
|
|
42
|
+
# rows. Wave 9 invariant: every Account has EXACTLY ONE system
|
|
43
|
+
# actor (created by `Account.create_with_owner`). Enforce it at
|
|
44
|
+
# the DB level with a partial unique index over the system rows.
|
|
45
|
+
add_index :accounts_memberships, :account_id, unique: true,
|
|
46
|
+
where: "role = 'system'",
|
|
47
|
+
name: "index_accounts_memberships_one_system_per_account"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "accounts/version"
|
|
4
|
+
require "accounts/configuration"
|
|
5
|
+
require "accounts/engine"
|
|
6
|
+
require "accounts/concerns/account_scoped"
|
|
7
|
+
require "accounts/concerns/authorization"
|
|
8
|
+
|
|
9
|
+
module Accounts
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def configure
|
|
18
|
+
yield configuration
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Accounts
|
|
6
|
+
# Mix into any model whose rows belong to a single Account. Adds:
|
|
7
|
+
#
|
|
8
|
+
# - `belongs_to :account, class_name: "Accounts::Account"`
|
|
9
|
+
# - a default_scope filtered to `Accounts::Current.account`. When
|
|
10
|
+
# `Accounts::Current.account` is unset, the scope returns `none`
|
|
11
|
+
# (an empty relation) — see "fail-closed" below.
|
|
12
|
+
# - presence validation on `account`
|
|
13
|
+
#
|
|
14
|
+
# class AuditEntry < ApplicationRecord
|
|
15
|
+
# include Accounts::AccountScoped
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Accounts::Current.account = account
|
|
19
|
+
# AuditEntry.create!(action: "...") # account_id auto-assigned
|
|
20
|
+
# AuditEntry.all # only this account's rows
|
|
21
|
+
#
|
|
22
|
+
# ### Fail-closed default
|
|
23
|
+
#
|
|
24
|
+
# When `Accounts::Current.account` is nil, the default_scope returns
|
|
25
|
+
# `none` rather than `all`. Rationale: the alternative (return all
|
|
26
|
+
# rows across all tenants when no account is bound) is the canonical
|
|
27
|
+
# multi-tenant data-leak bug. A forgotten `Current.account=` in a
|
|
28
|
+
# background job, an admin tool, or a Rails console session would
|
|
29
|
+
# otherwise hand the operator another tenant's data without warning.
|
|
30
|
+
# Fail-closed surfaces "I have no rows" — a noisy symptom the
|
|
31
|
+
# developer fixes immediately — instead of a silent leak.
|
|
32
|
+
#
|
|
33
|
+
# Opt-out paths (use deliberately):
|
|
34
|
+
# * `.unscoped` — full Active Record bypass; loses every default
|
|
35
|
+
# scope including soft-delete + sluggable; reserved for
|
|
36
|
+
# platform-admin / impersonation flows gated on
|
|
37
|
+
# `Auth::Current.identity.staff?`.
|
|
38
|
+
# * `.with_no_account_scope` — returns the relation as if the
|
|
39
|
+
# model were not account-scoped; preserves any other
|
|
40
|
+
# default_scopes the model declares; intended for seed scripts,
|
|
41
|
+
# migrations, and platform tooling.
|
|
42
|
+
#
|
|
43
|
+
# Background jobs MUST set `Accounts::Current.account =` before
|
|
44
|
+
# querying, or use `.with_no_account_scope` explicitly. The shape:
|
|
45
|
+
#
|
|
46
|
+
# class IngestionJob < ApplicationJob
|
|
47
|
+
# def perform(account_id, payload)
|
|
48
|
+
# Accounts::Current.account = Accounts::Account.find(account_id)
|
|
49
|
+
# AuditEntry.create!(...) # auto-scoped
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
module AccountScoped
|
|
53
|
+
extend ActiveSupport::Concern
|
|
54
|
+
|
|
55
|
+
included do
|
|
56
|
+
belongs_to :account, class_name: "Accounts::Account"
|
|
57
|
+
|
|
58
|
+
# The default_scope ALWAYS applies a where(:account_id) clause —
|
|
59
|
+
# either filtered to the current account or to NULL. The model
|
|
60
|
+
# validates `account` presence and the `belongs_to` adds a NOT
|
|
61
|
+
# NULL guard at the application layer, so `where(account_id: nil)`
|
|
62
|
+
# is a never-matching sentinel — fail-closed without using
|
|
63
|
+
# `none`. The shape is deliberate: `unscope(where: :account_id)`
|
|
64
|
+
# in `with_no_account_scope` cleanly reverses this. Using
|
|
65
|
+
# `none` would create a NullRelation that `unscope` cannot un-do,
|
|
66
|
+
# which would defeat the documented opt-out path.
|
|
67
|
+
default_scope -> {
|
|
68
|
+
account = Accounts::Current.account
|
|
69
|
+
if account
|
|
70
|
+
where(account_id: account.id)
|
|
71
|
+
else
|
|
72
|
+
where(account_id: nil)
|
|
73
|
+
end
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
validates :account, presence: true
|
|
77
|
+
|
|
78
|
+
before_validation :assign_current_account, on: :create
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class_methods do
|
|
82
|
+
# Returns the relation as it would be without the AccountScoped
|
|
83
|
+
# default_scope, preserving any other default_scopes the model
|
|
84
|
+
# declares. Use deliberately — name-it-loud opt-out for seed
|
|
85
|
+
# scripts, migrations, and platform tooling.
|
|
86
|
+
def with_no_account_scope
|
|
87
|
+
unscope(where: :account_id)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def assign_current_account
|
|
94
|
+
self.account_id ||= Accounts::Current.account&.id if Accounts::Current.account
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/concern"
|
|
4
|
+
|
|
5
|
+
module Accounts
|
|
6
|
+
# Controller concern that enforces tenant access on every action by
|
|
7
|
+
# default. Mix into the host's ApplicationController (or a base
|
|
8
|
+
# account-scoped controller) and use the class-method helpers to
|
|
9
|
+
# opt out per-controller.
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationController < ActionController::Base
|
|
12
|
+
# include Accounts::Authorization
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# class PublicPagesController < ApplicationController
|
|
16
|
+
# disallow_account_scope # no membership check
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# class OnboardingController < ApplicationController
|
|
20
|
+
# require_access_without_membership
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Default-on `before_action :ensure_account_access` checks that the
|
|
24
|
+
# current Account is active AND the current Membership is active.
|
|
25
|
+
# Otherwise: 403 (or redirects on HTML).
|
|
26
|
+
#
|
|
27
|
+
# `ensure_admin` and `ensure_staff` are opt-in helpers controllers
|
|
28
|
+
# call from their own `before_action` — they don't run by default.
|
|
29
|
+
# `ensure_admin` checks the per-Account role (owner/admin);
|
|
30
|
+
# `ensure_staff` checks the platform-admin flag on Identity (set
|
|
31
|
+
# via `Auth::Identity#staff?`). Two different powers — keep them
|
|
32
|
+
# distinct.
|
|
33
|
+
module Authorization
|
|
34
|
+
extend ActiveSupport::Concern
|
|
35
|
+
|
|
36
|
+
included do
|
|
37
|
+
before_action :ensure_account_access if respond_to?(:before_action)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class_methods do
|
|
41
|
+
# Skip the default tenant access check for this controller. Use
|
|
42
|
+
# for public pages (sign-in, marketing, OAuth callbacks).
|
|
43
|
+
def disallow_account_scope(**options)
|
|
44
|
+
skip_before_action :ensure_account_access, **options
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Sign-up + onboarding flows: the Identity is signed in but
|
|
48
|
+
# doesn't have a Membership yet. Skip the access check, but
|
|
49
|
+
# redirect away if a Membership IS present (otherwise the user
|
|
50
|
+
# bounces between sign-up and the dashboard).
|
|
51
|
+
def require_access_without_membership(**options)
|
|
52
|
+
skip_before_action :ensure_account_access, **options
|
|
53
|
+
before_action :redirect_existing_membership, **options
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def ensure_account_access
|
|
60
|
+
return if account_access_allowed?
|
|
61
|
+
|
|
62
|
+
respond_to do |format|
|
|
63
|
+
format.html { redirect_to(authorization_redirect_path) }
|
|
64
|
+
format.json { head :forbidden }
|
|
65
|
+
format.any { head :forbidden }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def account_access_allowed?
|
|
70
|
+
return false unless Accounts::Current.account&.active?
|
|
71
|
+
|
|
72
|
+
membership = Accounts::Current.membership
|
|
73
|
+
membership.present? && membership.active && !membership.system?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Per-account admin (owner OR admin). Use as a controller
|
|
77
|
+
# `before_action :ensure_admin` for in-account admin tooling
|
|
78
|
+
# (member management, billing settings, etc.).
|
|
79
|
+
def ensure_admin
|
|
80
|
+
head :forbidden unless Accounts::Current.membership&.admin?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Platform admin (Identity#staff?). Use as a controller
|
|
84
|
+
# `before_action :ensure_staff` for support tooling that has to
|
|
85
|
+
# bypass the per-account scope (e.g. impersonation, cross-account
|
|
86
|
+
# search). Distinct from `ensure_admin`.
|
|
87
|
+
def ensure_staff
|
|
88
|
+
identity = defined?(Auth::Current) ? Auth::Current.identity : nil
|
|
89
|
+
head :forbidden unless identity&.staff?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def redirect_existing_membership
|
|
93
|
+
return unless Accounts::Current.membership
|
|
94
|
+
|
|
95
|
+
respond_to do |format|
|
|
96
|
+
format.html { redirect_to(Accounts.configuration.after_account_create_url) }
|
|
97
|
+
format.any { head :found }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def authorization_redirect_path
|
|
102
|
+
Accounts.configuration.after_account_create_url
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Accounts
|
|
4
|
+
# Engine-scoped configuration. Override in
|
|
5
|
+
# config/initializers/accounts.rb of the host application:
|
|
6
|
+
#
|
|
7
|
+
# Accounts.configure do |c|
|
|
8
|
+
# c.incineration_grace_period = 30.days
|
|
9
|
+
# c.after_account_create_url = "/dashboard"
|
|
10
|
+
# end
|
|
11
|
+
class Configuration
|
|
12
|
+
attr_accessor :incineration_grace_period,
|
|
13
|
+
:after_account_create_url
|
|
14
|
+
# Follow-up generators that add knobs (account_owner_role, default_account_locale) declare their attr_accessor here.
|
|
15
|
+
# seams:insertion-point accounts.configuration.attributes
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
# How long a cancelled account lingers before incineration (hard
|
|
19
|
+
# delete). Hosts use this to power dunning + grace-period UX.
|
|
20
|
+
@incineration_grace_period = 30 * 24 * 60 * 60 # 30 days
|
|
21
|
+
@after_account_create_url = "/"
|
|
22
|
+
# Follow-up generators that add defaults for new attributes (matching accounts.configuration.attributes) splice them here.
|
|
23
|
+
# seams:insertion-point accounts.configuration.defaults
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Accounts
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace Accounts
|
|
6
|
+
|
|
7
|
+
config.generators do |g|
|
|
8
|
+
g.test_framework :rspec
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
initializer "accounts.register_events" do
|
|
12
|
+
Seams::EventRegistry.register("account.created.accounts", emitted_by: "Accounts")
|
|
13
|
+
Seams::EventRegistry.register("account.cancelled.accounts", emitted_by: "Accounts")
|
|
14
|
+
Seams::EventRegistry.register("membership.created.accounts", emitted_by: "Accounts")
|
|
15
|
+
Seams::EventRegistry.register("membership.role_changed.accounts", emitted_by: "Accounts")
|
|
16
|
+
Seams::EventRegistry.register("membership.removed.accounts", emitted_by: "Accounts")
|
|
17
|
+
# Follow-up generators that emit new accounts events (account.upgraded.accounts, etc.) register them here.
|
|
18
|
+
# seams:insertion-point accounts.engine.events
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
initializer "accounts.append_migrations" do |app|
|
|
22
|
+
unless app.root == root
|
|
23
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
|
24
|
+
app.config.paths["db/migrate"] << expanded_path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Follow-up generators that need their own initializer block declare it here, ahead of the cross-engine dependency check.
|
|
30
|
+
# seams:insertion-point accounts.engine.initializers
|
|
31
|
+
|
|
32
|
+
# Boot-time dependency assertion. Accounts requires Auth: an
|
|
33
|
+
# Accounts::Membership with an `identity_id` is meaningless
|
|
34
|
+
# without an `auth_identities` row to point at. We enforce this
|
|
35
|
+
# at app boot rather than at first failed query so the operator
|
|
36
|
+
# gets a clear "install seams:auth" error instead of a deep stack
|
|
37
|
+
# trace mid-request.
|
|
38
|
+
config.after_initialize do
|
|
39
|
+
missing = []
|
|
40
|
+
missing << "Auth::Identity (run: bin/rails generate seams:auth)" unless defined?(::Auth::Identity)
|
|
41
|
+
|
|
42
|
+
if missing.any?
|
|
43
|
+
raise <<~MSG
|
|
44
|
+
[seams accounts] missing required cross-engine dependency:
|
|
45
|
+
#{missing.join("\n ")}
|
|
46
|
+
|
|
47
|
+
The accounts engine joins Auth::Identity rows to
|
|
48
|
+
Accounts::Account rows; it cannot run without auth. Generate
|
|
49
|
+
the auth engine and re-run db:migrate, or remove accounts
|
|
50
|
+
with `bin/rails generate seams:remove accounts --force`.
|
|
51
|
+
MSG
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Factories for Accounts engine specs. Sequences keep names unique
|
|
4
|
+
# across the full spec run. The :membership factory associates with
|
|
5
|
+
# the auth engine's :auth_identity factory — ensure both engines
|
|
6
|
+
# are loaded in the dummy app or include the auth_identities table
|
|
7
|
+
# in this engine's dummy schema.
|
|
8
|
+
FactoryBot.define do
|
|
9
|
+
factory :account, class: "Accounts::Account" do
|
|
10
|
+
sequence(:name) { |n| "Account #{n}" }
|
|
11
|
+
sequence(:external_account_id) { |n| 1_000_000 + n }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
factory :membership, class: "Accounts::Membership" do
|
|
15
|
+
association :account
|
|
16
|
+
association :identity, factory: :auth_identity
|
|
17
|
+
sequence(:name) { |n| "Member #{n}" }
|
|
18
|
+
role { "member" }
|
|
19
|
+
active { true }
|
|
20
|
+
|
|
21
|
+
# Denormalises identity_id from the association rather than
|
|
22
|
+
# relying on belongs_to magic — Membership has no `belongs_to
|
|
23
|
+
# :identity` because the auth engine and accounts engine are
|
|
24
|
+
# peer engines (no cross-engine model access at the
|
|
25
|
+
# ActiveRecord level).
|
|
26
|
+
after(:build) do |membership, evaluator|
|
|
27
|
+
membership.identity_id = evaluator.identity&.id
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
transient do
|
|
31
|
+
identity { nil }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
factory :owner_membership do
|
|
35
|
+
role { "owner" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
factory :admin_membership do
|
|
39
|
+
role { "admin" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
factory :system_membership do
|
|
43
|
+
role { "system" }
|
|
44
|
+
identity { nil }
|
|
45
|
+
identity_id { nil }
|
|
46
|
+
name { "System" }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Accounts::Account do
|
|
6
|
+
describe "validations" do
|
|
7
|
+
it "requires a name" do
|
|
8
|
+
account = described_class.new
|
|
9
|
+
expect(account).not_to be_valid
|
|
10
|
+
expect(account.errors[:name]).to include("can't be blank")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "scopes" do
|
|
15
|
+
it ".active excludes cancelled accounts" do
|
|
16
|
+
live = described_class.create!(name: "Live")
|
|
17
|
+
cancelled = described_class.create!(name: "Cancelled", cancelled_at: 1.day.ago)
|
|
18
|
+
|
|
19
|
+
expect(described_class.active).to include(live)
|
|
20
|
+
expect(described_class.active).not_to include(cancelled)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#active?" do
|
|
25
|
+
it "is true when not cancelled and not incinerated" do
|
|
26
|
+
expect(described_class.new).to be_active
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "is false when cancelled_at is set" do
|
|
30
|
+
expect(described_class.new(cancelled_at: 1.day.ago)).not_to be_active
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "is false when incinerated_at is set" do
|
|
34
|
+
expect(described_class.new(incinerated_at: 1.day.ago)).not_to be_active
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "#assign_external_account_id" do
|
|
39
|
+
it "auto-assigns external_account_id on create when blank" do
|
|
40
|
+
account = described_class.create!(name: "Acme")
|
|
41
|
+
expect(account.external_account_id).to be_a(Integer)
|
|
42
|
+
expect(account.external_account_id).to be > 0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe ".create_with_owner" do
|
|
47
|
+
let(:identity) { Auth::Identity.create!(email: "owner-#{SecureRandom.hex(4)}@example.com", password: "verysecret") }
|
|
48
|
+
let(:owner) { Struct.new(:identity, :name).new(identity, "Ada") }
|
|
49
|
+
|
|
50
|
+
it "creates an account, a system membership, and an owner membership" do
|
|
51
|
+
account = described_class.create_with_owner(account: { name: "Acme" }, owner: owner)
|
|
52
|
+
expect(account.memberships.pluck(:role)).to match_array(%w[system owner])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "rolls back the account when the owner membership fails to save" do
|
|
56
|
+
bad_owner = Struct.new(:identity, :name).new(identity, nil)
|
|
57
|
+
|
|
58
|
+
expect {
|
|
59
|
+
described_class.create_with_owner(account: { name: "Bad" }, owner: bad_owner)
|
|
60
|
+
}.to raise_error(ActiveRecord::RecordInvalid)
|
|
61
|
+
.and(change(described_class, :count).by(0))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|