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,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
class InvitationsController < ApplicationController
|
|
5
|
+
include Teams::Authorization
|
|
6
|
+
|
|
7
|
+
before_action :set_team, only: %i[index create destroy]
|
|
8
|
+
before_action :require_team_admin!, only: %i[create destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@invitations = @team.invitations.pending
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create
|
|
15
|
+
invitation = @team.invitations.create!(invitation_params)
|
|
16
|
+
Seams::Events::Publisher.publish(
|
|
17
|
+
"invitation.sent.teams",
|
|
18
|
+
invitation_id: invitation.id,
|
|
19
|
+
team_id: @team.id,
|
|
20
|
+
email: invitation.email,
|
|
21
|
+
role: invitation.role,
|
|
22
|
+
token: invitation.token
|
|
23
|
+
)
|
|
24
|
+
redirect_to team_invitations_path(@team), notice: "Invitation sent"
|
|
25
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
26
|
+
redirect_to team_invitations_path(@team), alert: e.message
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def destroy
|
|
30
|
+
invitation = @team.invitations.find(params[:id])
|
|
31
|
+
invitation.destroy
|
|
32
|
+
redirect_to team_invitations_path(@team), notice: "Invitation revoked"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# GET /invitations/accept/:token — show the confirmation page.
|
|
36
|
+
def accept_form
|
|
37
|
+
@invitation = Teams::Invitation.find_by!(token: params[:token])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# POST /invitations/accept/:token — perform the accept.
|
|
41
|
+
# Wraps the lookup in a row lock and short-circuits if the
|
|
42
|
+
# invitation has already been accepted, so a double-click on the
|
|
43
|
+
# email link returns a friendly redirect instead of a 500.
|
|
44
|
+
def accept
|
|
45
|
+
Teams::Invitation.transaction do
|
|
46
|
+
@invitation = Teams::Invitation.lock.find_by!(token: params[:token])
|
|
47
|
+
|
|
48
|
+
if @invitation.accepted?
|
|
49
|
+
return redirect_to team_path(@invitation.team), notice: "You're already a member"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if @invitation.expired?
|
|
53
|
+
return redirect_to root_path, alert: "Invitation expired"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@invitation.team.memberships.create!(identity_id: current_identity_id, role: @invitation.role)
|
|
57
|
+
@invitation.update!(accepted_at: Time.current)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
Seams::Events::Publisher.publish(
|
|
61
|
+
"invitation.accepted.teams",
|
|
62
|
+
team_id: @invitation.team_id,
|
|
63
|
+
identity_id: current_identity_id,
|
|
64
|
+
invitation_id: @invitation.id
|
|
65
|
+
)
|
|
66
|
+
redirect_to team_path(@invitation.team), notice: "Joined #{@invitation.team.name}"
|
|
67
|
+
rescue ActiveRecord::RecordNotUnique
|
|
68
|
+
redirect_to team_path(@invitation.team), notice: "You're already a member"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def set_team
|
|
74
|
+
@team = Teams::Team.find(params[:team_id])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# `permit` deliberately omits `:role` — Brakeman flags it as
|
|
78
|
+
# mass-assignment, and even with server-side coercion the
|
|
79
|
+
# permit-list reads as "we accept whatever role the form posts".
|
|
80
|
+
# Role is extracted separately via `safe_role` and merged in.
|
|
81
|
+
def invitation_params
|
|
82
|
+
params.require(:invitation).permit(:email).merge(role: safe_role)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def safe_role
|
|
86
|
+
candidate = params.dig(:invitation, :role).to_s
|
|
87
|
+
Teams::Membership::ROLES.include?(candidate) ? candidate : "member"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Resolves the signed-in human's id from `Auth::Current.identity`
|
|
91
|
+
# (the Auth engine's per-request namespace). Gated on
|
|
92
|
+
# `defined?(Auth::Current)` so it's safe in hosts that don't ship
|
|
93
|
+
# auth. Override in your host if you wire auth differently.
|
|
94
|
+
def current_identity_id
|
|
95
|
+
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
96
|
+
return Auth::Current.identity.id
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
class MembershipsController < ApplicationController
|
|
5
|
+
include Teams::Authorization
|
|
6
|
+
|
|
7
|
+
before_action :set_team
|
|
8
|
+
before_action :require_team_member!, only: %i[index]
|
|
9
|
+
before_action :require_team_admin!, only: %i[create destroy]
|
|
10
|
+
|
|
11
|
+
def index
|
|
12
|
+
@memberships = @team.memberships
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def create
|
|
16
|
+
membership = @team.memberships.create!(membership_params)
|
|
17
|
+
Seams::Events::Publisher.publish(
|
|
18
|
+
"team.member_joined.teams",
|
|
19
|
+
team_id: @team.id, identity_id: membership.identity_id, role: membership.role
|
|
20
|
+
)
|
|
21
|
+
redirect_to team_memberships_path(@team), notice: "Member added"
|
|
22
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
23
|
+
redirect_to team_memberships_path(@team), alert: e.message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def destroy
|
|
27
|
+
membership = @team.memberships.find(params[:id])
|
|
28
|
+
membership.destroy
|
|
29
|
+
Seams::Events::Publisher.publish(
|
|
30
|
+
"team.member_left.teams", team_id: @team.id, identity_id: membership.identity_id
|
|
31
|
+
)
|
|
32
|
+
redirect_to team_memberships_path(@team), notice: "Member removed"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def set_team
|
|
38
|
+
@team = Teams::Team.find(params[:team_id])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# `permit` deliberately omits `:role` — Brakeman flags it as
|
|
42
|
+
# mass-assignment, and even with server-side coercion the
|
|
43
|
+
# permit-list reads as "we accept whatever role the form posts".
|
|
44
|
+
# Role is extracted separately via `safe_role` and merged in.
|
|
45
|
+
def membership_params
|
|
46
|
+
params.require(:membership).permit(:identity_id).merge(role: safe_role)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def safe_role
|
|
50
|
+
candidate = params.dig(:membership, :role).to_s
|
|
51
|
+
Teams::Membership::ROLES.include?(candidate) ? candidate : "member"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
class TeamsController < ApplicationController
|
|
5
|
+
before_action :set_team, only: %i[show edit update destroy]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@teams = Teams::Team.joins(:memberships).where(memberships: { identity_id: current_identity_id })
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def show; end
|
|
12
|
+
def new
|
|
13
|
+
@team = Teams::Team.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def edit; end
|
|
17
|
+
|
|
18
|
+
def create
|
|
19
|
+
@team = Teams::Team.new(team_params)
|
|
20
|
+
Teams::Team.transaction do
|
|
21
|
+
@team.save!
|
|
22
|
+
@team.memberships.create!(identity_id: current_identity_id, role: "owner")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Seams::Events::Publisher.publish(
|
|
26
|
+
"team.created.teams", team_id: @team.id, creator_identity_id: current_identity_id
|
|
27
|
+
)
|
|
28
|
+
redirect_to @team, notice: "Team created"
|
|
29
|
+
rescue ActiveRecord::RecordInvalid
|
|
30
|
+
render :new, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def update
|
|
34
|
+
if @team.update(team_params)
|
|
35
|
+
redirect_to @team, notice: "Team updated"
|
|
36
|
+
else
|
|
37
|
+
render :edit, status: :unprocessable_entity
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def destroy
|
|
42
|
+
@team.destroy
|
|
43
|
+
redirect_to teams_path, notice: "Team deleted"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def set_team
|
|
49
|
+
@team = Teams::Team.find(params[:id])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def team_params
|
|
53
|
+
params.require(:team).permit(:name, :slug)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolves the signed-in human's id from `Auth::Current.identity`
|
|
57
|
+
# (the Auth engine's per-request namespace). Gated on
|
|
58
|
+
# `defined?(Auth::Current)` so it's safe in hosts that don't ship
|
|
59
|
+
# auth. Override in your host if you wire auth differently.
|
|
60
|
+
def current_identity_id
|
|
61
|
+
if defined?(Auth::Current) && Auth::Current.respond_to?(:identity) && Auth::Current.identity
|
|
62
|
+
return Auth::Current.identity.id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
# Sends the email a team invitation generates. The default invocation
|
|
5
|
+
# is `InvitationMailer.invite(invitation_id)` from
|
|
6
|
+
# `Teams::InvitationSubscriber` — both the controller and any other
|
|
7
|
+
# caller can also invoke it directly.
|
|
8
|
+
#
|
|
9
|
+
# Override the body by dropping a file at
|
|
10
|
+
# `app/views/teams/invitation_mailer/invite.text.erb` (or .html.erb)
|
|
11
|
+
# in the host application.
|
|
12
|
+
class InvitationMailer < ActionMailer::Base
|
|
13
|
+
default from: -> { Teams.configuration.invitation_mailer_from }
|
|
14
|
+
|
|
15
|
+
def invite(invitation_id)
|
|
16
|
+
@invitation = Teams::Invitation.find(invitation_id)
|
|
17
|
+
@team = @invitation.team
|
|
18
|
+
@accept_url = build_accept_url(@invitation.token)
|
|
19
|
+
|
|
20
|
+
mail(
|
|
21
|
+
to: @invitation.email,
|
|
22
|
+
subject: "You're invited to join #{@team.name}"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_accept_url(token)
|
|
29
|
+
base = Teams.configuration.host_url.to_s
|
|
30
|
+
base = base.sub(/\/+\z/, "")
|
|
31
|
+
"#{base}/teams/invitations/accept/#{token}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
# ActiveSupport::CurrentAttributes namespace for the Teams engine.
|
|
5
|
+
# Set once per request by the host (typically a before_action that
|
|
6
|
+
# resolves the team from the URL or session); readable from anywhere
|
|
7
|
+
# downstream without explicit threading.
|
|
8
|
+
#
|
|
9
|
+
# `Teams::AccountScoped` reads `Teams::Current.team` to filter rows
|
|
10
|
+
# to the current team. When unset, the default_scope short-circuits
|
|
11
|
+
# to a no-op so background jobs that don't bind a team still see
|
|
12
|
+
# their full set — wire `Teams::Current.team =` into your job's
|
|
13
|
+
# #perform if you need scoping there.
|
|
14
|
+
#
|
|
15
|
+
# Peer to `Auth::Current` and `Accounts::Current`. No cross-engine
|
|
16
|
+
# cascade — Teams::Current owns only the current team. The
|
|
17
|
+
# signed-in identity lives in `Auth::Current.identity`; the active
|
|
18
|
+
# tenant account lives in `Accounts::Current.account`.
|
|
19
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
20
|
+
attribute :team
|
|
21
|
+
|
|
22
|
+
def with_team(value, &)
|
|
23
|
+
with(team: value, &)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def without_team(&)
|
|
27
|
+
with(team: nil, &)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
class Invitation < ApplicationRecord
|
|
5
|
+
self.table_name = "team_invitations"
|
|
6
|
+
|
|
7
|
+
belongs_to :team, class_name: "Teams::Team"
|
|
8
|
+
|
|
9
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
10
|
+
validates :token, presence: true, uniqueness: true
|
|
11
|
+
validates :role, inclusion: { in: Teams::Membership::ROLES }
|
|
12
|
+
|
|
13
|
+
before_validation :assign_token, on: :create
|
|
14
|
+
before_validation :assign_expiry, on: :create
|
|
15
|
+
|
|
16
|
+
scope :pending, -> { where(accepted_at: nil).where("expires_at > ?", Time.current) }
|
|
17
|
+
|
|
18
|
+
def expired?
|
|
19
|
+
expires_at <= Time.current
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def accepted?
|
|
23
|
+
accepted_at.present?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def assign_token
|
|
29
|
+
self.token ||= SecureRandom.urlsafe_base64(32)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def assign_expiry
|
|
33
|
+
self.expires_at ||= Time.current + Teams.configuration.invitation_ttl
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
# Joins a Teams::Team to an Auth::Identity with a role. Wave 9 made
|
|
5
|
+
# teams a peer engine to accounts: the join is direct to Identity
|
|
6
|
+
# (not to an Accounts::Membership). `identity_id` is a bare bigint
|
|
7
|
+
# FK with no `belongs_to :identity` because Auth::Identity lives in
|
|
8
|
+
# a sibling engine and cross-engine ActiveRecord access is forbidden
|
|
9
|
+
# by the Seams/NoCrossEngineModelAccess cop. Look up the human via
|
|
10
|
+
# the auth engine's API or by `Auth::Identity.find(identity_id)` from
|
|
11
|
+
# host code.
|
|
12
|
+
class Membership < ApplicationRecord
|
|
13
|
+
self.table_name = "team_memberships"
|
|
14
|
+
|
|
15
|
+
# Teams roles are independent of Accounts roles by design — a
|
|
16
|
+
# team is its own RBAC unit. Hosts that want a single role across
|
|
17
|
+
# both should denormalise that themselves.
|
|
18
|
+
ROLES = %w[owner admin member].freeze
|
|
19
|
+
|
|
20
|
+
belongs_to :team, class_name: "Teams::Team"
|
|
21
|
+
|
|
22
|
+
validates :identity_id, presence: true, uniqueness: { scope: :team_id }
|
|
23
|
+
validates :role, inclusion: { in: ROLES }
|
|
24
|
+
|
|
25
|
+
scope :owners, -> { where(role: "owner") }
|
|
26
|
+
scope :admins, -> { where(role: %w[owner admin]) }
|
|
27
|
+
|
|
28
|
+
def owner?
|
|
29
|
+
role == "owner"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def admin?
|
|
33
|
+
%w[owner admin].include?(role)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
class Team < ApplicationRecord
|
|
5
|
+
self.table_name = "teams"
|
|
6
|
+
|
|
7
|
+
has_many :memberships, class_name: "Teams::Membership", foreign_key: :team_id, dependent: :destroy
|
|
8
|
+
has_many :invitations, class_name: "Teams::Invitation", dependent: :destroy
|
|
9
|
+
|
|
10
|
+
validates :name, presence: true, length: { maximum: 100 }
|
|
11
|
+
validates :slug, presence: true, uniqueness: true,
|
|
12
|
+
format: { with: /\A[a-z0-9-]+\z/, message: "may only contain lowercase letters, digits and dashes" }
|
|
13
|
+
|
|
14
|
+
before_validation :assign_slug, on: :create
|
|
15
|
+
|
|
16
|
+
def owner_membership
|
|
17
|
+
memberships.find_by(role: "owner")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Predicate for "is this Identity a member of this team?". Pass the
|
|
21
|
+
# Auth::Identity's id (the team_memberships.identity_id column).
|
|
22
|
+
def member?(identity_id)
|
|
23
|
+
memberships.exists?(identity_id: identity_id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def assign_slug
|
|
29
|
+
self.slug ||= name.to_s.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(^-|-$)/, "")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
# Consumes Teams events. On +invitation.sent.teams+:
|
|
5
|
+
# 1. Resolves the Teams::Invitation by id from the payload.
|
|
6
|
+
# 2. Enqueues Teams::InvitationMailer.invite — `deliver_later` so
|
|
7
|
+
# the publisher's transaction isn't blocked on SMTP.
|
|
8
|
+
#
|
|
9
|
+
# +.attach!+ is idempotent — Rails reload won't double-subscribe, and
|
|
10
|
+
# because we register the subscriber CLASS by its String name via
|
|
11
|
+
# +attach_class+, dispatch re-resolves the constant on every event so
|
|
12
|
+
# edits to +handle_invitation_sent+ take effect without a server restart.
|
|
13
|
+
class InvitationSubscriber
|
|
14
|
+
SUBSCRIBER_KEY = :teams_invitation_subscriber
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def attach!
|
|
18
|
+
Seams::Events::Publisher.attach_class(
|
|
19
|
+
SUBSCRIBER_KEY,
|
|
20
|
+
"invitation.sent.teams",
|
|
21
|
+
class_name: "Teams::InvitationSubscriber",
|
|
22
|
+
method_name: :handle_invitation_sent
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def handle_invitation_sent(payload)
|
|
29
|
+
invitation_id = payload[:invitation_id]
|
|
30
|
+
return unless invitation_id
|
|
31
|
+
|
|
32
|
+
Teams::InvitationMailer.invite(invitation_id).deliver_later
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%%# Default invitation email body. Override at %>
|
|
2
|
+
<%%# app/views/teams/invitation_mailer/invite.text.erb in your host. %>
|
|
3
|
+
You've been invited to join <%%= @team.name %>.
|
|
4
|
+
|
|
5
|
+
Accept the invitation here:
|
|
6
|
+
<%%= @accept_url %>
|
|
7
|
+
|
|
8
|
+
If you don't recognise this team, you can ignore this email.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%%# Pending + sent invitations for a team. Owners + admins can %>
|
|
2
|
+
<%%# revoke pending invitations; the recipient accepts via the %>
|
|
3
|
+
<%%# token-keyed top-level route (/invitations/accept/:token). %>
|
|
4
|
+
<h1>Invitations — <%%= @team.name %></h1>
|
|
5
|
+
|
|
6
|
+
<%%= link_to "← Back to team", @team %>
|
|
7
|
+
|
|
8
|
+
<h2>Send a new invitation</h2>
|
|
9
|
+
<%%= form_with url: team_invitations_path(@team) do |form| %>
|
|
10
|
+
<p>
|
|
11
|
+
<%%= form.label :email %>
|
|
12
|
+
<%%= form.email_field :email, required: true %>
|
|
13
|
+
</p>
|
|
14
|
+
<p>
|
|
15
|
+
<%%= form.label :role %>
|
|
16
|
+
<%%= form.select :role, %w[member admin] %>
|
|
17
|
+
</p>
|
|
18
|
+
<%%= form.submit "Send invite" %>
|
|
19
|
+
<%% end %>
|
|
20
|
+
|
|
21
|
+
<h2>Pending</h2>
|
|
22
|
+
<%% if @invitations.empty? %>
|
|
23
|
+
<p>No pending invitations.</p>
|
|
24
|
+
<%% else %>
|
|
25
|
+
<table>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr><th>Email</th><th>Role</th><th>Expires</th><th></th></tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
<%% @invitations.each do |invitation| %>
|
|
31
|
+
<tr>
|
|
32
|
+
<td><%%= invitation.email %></td>
|
|
33
|
+
<td><%%= invitation.role.titleize %></td>
|
|
34
|
+
<td><%%= invitation.expires_at.to_date %></td>
|
|
35
|
+
<td>
|
|
36
|
+
<%%= button_to "Revoke",
|
|
37
|
+
team_invitation_path(@team, invitation),
|
|
38
|
+
method: :delete %>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
<%% end %>
|
|
42
|
+
</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
<%% end %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%%# Members table. Owners + admins can remove a member or change %>
|
|
2
|
+
<%%# their role; members read-only see the list. The Identity column %>
|
|
3
|
+
<%%# shows the raw identity_id — hosts that want to render the human's %>
|
|
4
|
+
<%%# email/name should override this view and look the Identity up %>
|
|
5
|
+
<%%# themselves (cross-engine boundary keeps Teams from joining to %>
|
|
6
|
+
<%%# auth_identities at the data layer). %>
|
|
7
|
+
<h1>Members of <%%= @team.name %></h1>
|
|
8
|
+
|
|
9
|
+
<%%= link_to "← Back to team", @team %>
|
|
10
|
+
|
|
11
|
+
<table>
|
|
12
|
+
<thead>
|
|
13
|
+
<tr><th>Identity</th><th>Role</th><th>Joined</th><th></th></tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
<%% @memberships.each do |membership| %>
|
|
17
|
+
<tr>
|
|
18
|
+
<td><%%= membership.identity_id %></td>
|
|
19
|
+
<td><%%= membership.role.titleize %></td>
|
|
20
|
+
<td><%%= membership.created_at.to_date %></td>
|
|
21
|
+
<td>
|
|
22
|
+
<%% if can_manage_team_members? %>
|
|
23
|
+
<%%= button_to "Remove",
|
|
24
|
+
team_membership_path(@team, membership),
|
|
25
|
+
method: :delete,
|
|
26
|
+
data: { confirm: "Remove this member?" } %>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</td>
|
|
29
|
+
</tr>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</tbody>
|
|
32
|
+
</table>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<%%# Team settings — name + slug. Roles + member operations live %>
|
|
2
|
+
<%%# under the memberships endpoint, not here. %>
|
|
3
|
+
<h1>Settings — <%%= @team.name %></h1>
|
|
4
|
+
|
|
5
|
+
<%%= form_with model: @team, url: team_path(@team), method: :patch do |form| %>
|
|
6
|
+
<%% if @team.errors.any? %>
|
|
7
|
+
<ul class="errors">
|
|
8
|
+
<%% @team.errors.full_messages.each do |message| %>
|
|
9
|
+
<li><%%= message %></li>
|
|
10
|
+
<%% end %>
|
|
11
|
+
</ul>
|
|
12
|
+
<%% end %>
|
|
13
|
+
|
|
14
|
+
<p>
|
|
15
|
+
<%%= form.label :name %>
|
|
16
|
+
<%%= form.text_field :name, required: true %>
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p>
|
|
20
|
+
<%%= form.label :slug %>
|
|
21
|
+
<%%= form.text_field :slug, required: true %>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<%%= form.submit "Save" %>
|
|
25
|
+
<%% end %>
|
|
26
|
+
|
|
27
|
+
<%%= button_to "Delete team", team_path(@team), method: :delete,
|
|
28
|
+
data: { confirm: "This is irreversible. Continue?" } %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%%# Lists every team the signed-in user is a member of. Override at %>
|
|
2
|
+
<%%# app/views/teams/teams/index.html.erb in your host. %>
|
|
3
|
+
<h1>Your teams</h1>
|
|
4
|
+
|
|
5
|
+
<%%= link_to "New team", new_team_path %>
|
|
6
|
+
|
|
7
|
+
<%% if @teams.empty? %>
|
|
8
|
+
<p>You're not on any teams yet.</p>
|
|
9
|
+
<%% else %>
|
|
10
|
+
<ul>
|
|
11
|
+
<%% @teams.each do |team| %>
|
|
12
|
+
<li><%%= link_to team.name, team %> <small>(<%%= team.slug %>)</small></li>
|
|
13
|
+
<%% end %>
|
|
14
|
+
</ul>
|
|
15
|
+
<%% end %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%%# New team form — the creator becomes the owner via TeamsController#create. %>
|
|
2
|
+
<h1>New team</h1>
|
|
3
|
+
|
|
4
|
+
<%%= form_with model: @team, url: teams_path do |form| %>
|
|
5
|
+
<%% if @team.errors.any? %>
|
|
6
|
+
<ul class="errors">
|
|
7
|
+
<%% @team.errors.full_messages.each do |message| %>
|
|
8
|
+
<li><%%= message %></li>
|
|
9
|
+
<%% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
<%% end %>
|
|
12
|
+
|
|
13
|
+
<p>
|
|
14
|
+
<%%= form.label :name %>
|
|
15
|
+
<%%= form.text_field :name, required: true %>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p>
|
|
19
|
+
<%%= form.label :slug %>
|
|
20
|
+
<%%= form.text_field :slug, required: true %>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<%%= form.submit "Create team" %>
|
|
24
|
+
<%% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%%# Single team page. Top section = team metadata + edit link. %>
|
|
2
|
+
<%%# Members table linked to the memberships endpoint. Invitations %>
|
|
3
|
+
<%%# linked to the invitations endpoint when the engine ships them. %>
|
|
4
|
+
<h1><%%= @team.name %></h1>
|
|
5
|
+
<p><small><%%= @team.slug %></small></p>
|
|
6
|
+
|
|
7
|
+
<%%= link_to "Settings", edit_team_path(@team) %>
|
|
8
|
+
|
|
9
|
+
<h2>Members</h2>
|
|
10
|
+
<%%= link_to "Manage members", team_memberships_path(@team) %>
|
|
11
|
+
|
|
12
|
+
<h2>Invitations</h2>
|
|
13
|
+
<%% if defined?(team_invitations_path) %>
|
|
14
|
+
<%%= link_to "Manage invitations", team_invitations_path(@team) %>
|
|
15
|
+
<%% else %>
|
|
16
|
+
<p><em>Invitation feature not enabled (regenerate with --with=invitations).</em></p>
|
|
17
|
+
<%% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Teams::Engine.routes.draw do
|
|
4
|
+
# Follow-up generators that add admin-only or token-only routes splice them here, ahead of the canonical teams resource.
|
|
5
|
+
# seams:insertion-point teams.routes.before_teams
|
|
6
|
+
resources :teams, only: %i[index show new create edit update destroy] do
|
|
7
|
+
resources :memberships, only: %i[index create destroy]
|
|
8
|
+
resources :invitations, only: %i[index create destroy]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Token-only accept route — the recipient clicks a link from the
|
|
12
|
+
# invitation email and doesn't know the team_id. Lives at the top
|
|
13
|
+
# level so the URL is short and shareable.
|
|
14
|
+
get "/invitations/accept/:token", to: "invitations#accept_form", as: :accept_invitation
|
|
15
|
+
post "/invitations/accept/:token", to: "invitations#accept", as: :confirm_invitation
|
|
16
|
+
|
|
17
|
+
# Follow-up generators that add new top-level team routes (transfer, archive) splice them here.
|
|
18
|
+
# seams:insertion-point teams.routes.after_invitations
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What: creates the team_invitations table.
|
|
4
|
+
# Why: invitation lifecycle (sent / accepted / expired / revoked) is
|
|
5
|
+
# distinct from membership and we want to keep the audit trail
|
|
6
|
+
# even after the recipient joins.
|
|
7
|
+
# Risk: append-mostly. Token uniqueness is the security guarantee.
|
|
8
|
+
class CreateTeamInvitations < ActiveRecord::Migration[7.1]
|
|
9
|
+
def change
|
|
10
|
+
create_table :team_invitations do |t|
|
|
11
|
+
t.references :team, null: false, foreign_key: { to_table: :teams }, index: true
|
|
12
|
+
t.string :email, null: false
|
|
13
|
+
t.string :token, null: false
|
|
14
|
+
t.string :role, null: false, default: "member"
|
|
15
|
+
t.datetime :expires_at, null: false
|
|
16
|
+
t.datetime :accepted_at
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :team_invitations, :token, unique: true
|
|
21
|
+
add_index :team_invitations, %i[team_id email], unique: true,
|
|
22
|
+
where: "accepted_at IS NULL"
|
|
23
|
+
end
|
|
24
|
+
end
|