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.
Files changed (414) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +335 -0
  3. data/LICENSE +21 -0
  4. data/README.md +104 -0
  5. data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
  6. data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
  7. data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
  8. data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
  9. data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
  10. data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
  11. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
  12. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
  13. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
  14. data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
  15. data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
  16. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
  17. data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
  18. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
  19. data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
  20. data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
  21. data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
  22. data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
  23. data/lib/generators/seams/admin/admin_generator.rb +852 -0
  24. data/lib/generators/seams/admin/templates/README.md.tt +266 -0
  25. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
  26. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
  27. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
  28. data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
  29. data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
  30. data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
  31. data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
  32. data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
  33. data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
  34. data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
  35. data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
  36. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
  37. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
  38. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
  39. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
  40. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
  41. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
  42. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
  43. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
  44. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
  45. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
  46. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
  47. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
  48. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
  49. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
  50. data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
  51. data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
  52. data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
  53. data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
  54. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
  55. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
  56. data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
  57. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
  58. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
  59. data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
  60. data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
  61. data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
  62. data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
  63. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
  64. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
  65. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
  66. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
  67. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
  68. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
  69. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
  70. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
  71. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
  72. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
  73. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
  74. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
  75. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
  76. data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
  77. data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
  78. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
  79. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
  80. data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
  81. data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
  82. data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
  83. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
  84. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
  85. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
  86. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
  87. data/lib/generators/seams/auth/auth_generator.rb +311 -0
  88. data/lib/generators/seams/auth/templates/README.md.tt +289 -0
  89. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
  90. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
  91. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
  92. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
  93. data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
  94. data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
  95. data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
  96. data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
  97. data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
  98. data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
  99. data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
  100. data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
  101. data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
  102. data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
  103. data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
  104. data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
  105. data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
  106. data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
  107. data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
  108. data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
  109. data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
  110. data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
  111. data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
  112. data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
  113. data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
  114. data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
  115. data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
  116. data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
  117. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
  118. data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
  119. data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
  120. data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
  121. data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
  122. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
  123. data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
  124. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
  125. data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
  126. data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
  127. data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
  128. data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
  129. data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
  130. data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
  131. data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
  132. data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
  133. data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
  134. data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
  135. data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
  136. data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
  137. data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
  138. data/lib/generators/seams/billing/billing_generator.rb +476 -0
  139. data/lib/generators/seams/billing/templates/README.md.tt +355 -0
  140. data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
  141. data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
  142. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
  143. data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
  144. data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
  145. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
  146. data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
  147. data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
  148. data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
  149. data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
  150. data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
  151. data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
  152. data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
  153. data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
  154. data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
  155. data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
  156. data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
  157. data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
  158. data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
  159. data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
  160. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
  161. data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
  162. data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
  163. data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
  164. data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
  165. data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
  166. data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
  167. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
  168. data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
  169. data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
  170. data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
  171. data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
  172. data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
  173. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
  174. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
  175. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
  176. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
  177. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
  178. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
  179. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
  180. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
  181. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
  182. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
  183. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
  184. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
  185. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
  186. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
  187. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
  188. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
  189. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
  190. data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
  191. data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
  192. data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
  193. data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
  194. data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
  195. data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
  196. data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
  197. data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
  198. data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
  199. data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
  200. data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
  201. data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
  202. data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
  203. data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
  204. data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
  205. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
  206. data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
  207. data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
  208. data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
  209. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
  210. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
  211. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
  212. data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
  213. data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
  214. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
  215. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
  216. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
  217. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
  218. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
  219. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
  220. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
  221. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
  222. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
  223. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
  224. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
  225. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
  226. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
  227. data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
  228. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
  229. data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
  230. data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
  231. data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
  232. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
  233. data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
  234. data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
  235. data/lib/generators/seams/core/core_generator.rb +191 -0
  236. data/lib/generators/seams/core/templates/README.md.tt +45 -0
  237. data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
  238. data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
  239. data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
  240. data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
  241. data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
  242. data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
  243. data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
  244. data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
  245. data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
  246. data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
  247. data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
  248. data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
  249. data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
  250. data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
  251. data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
  252. data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
  253. data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
  254. data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
  255. data/lib/generators/seams/engine/engine_generator.rb +165 -0
  256. data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
  257. data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
  258. data/lib/generators/seams/engine/templates/README.md.tt +40 -0
  259. data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
  260. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  261. data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
  262. data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
  263. data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
  264. data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
  265. data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
  266. data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
  267. data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
  268. data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
  269. data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
  270. data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
  271. data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
  272. data/lib/generators/seams/install/install_generator.rb +211 -0
  273. data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
  274. data/lib/generators/seams/install/templates/Procfile.tt +14 -0
  275. data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
  276. data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
  277. data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
  278. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
  279. data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
  280. data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
  281. data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
  282. data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
  283. data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
  284. data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
  285. data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
  286. data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
  287. data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
  288. data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
  289. data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
  290. data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
  291. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
  292. data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
  293. data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
  294. data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
  295. data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
  296. data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
  297. data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
  298. data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
  299. data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
  300. data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
  301. data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
  302. data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
  303. data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
  304. data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
  305. data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
  306. data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
  307. data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
  308. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
  309. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
  310. data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
  311. data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
  312. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
  313. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
  314. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
  315. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
  316. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
  317. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
  318. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
  319. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
  320. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
  321. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
  322. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
  323. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
  324. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
  325. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
  326. data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
  327. data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
  328. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
  329. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
  330. data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
  331. data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
  332. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
  333. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
  334. data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
  335. data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
  336. data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
  337. data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
  338. data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
  339. data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
  340. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
  341. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
  342. data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
  343. data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
  344. data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
  345. data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
  346. data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
  347. data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
  348. data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
  349. data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
  350. data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
  351. data/lib/generators/seams/remove/remove_generator.rb +259 -0
  352. data/lib/generators/seams/teams/teams_generator.rb +298 -0
  353. data/lib/generators/seams/teams/templates/README.md.tt +88 -0
  354. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
  355. data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
  356. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
  357. data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
  358. data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
  359. data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
  360. data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
  361. data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
  362. data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
  363. data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
  364. data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
  365. data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
  366. data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
  367. data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
  368. data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
  369. data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
  370. data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
  371. data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
  372. data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
  373. data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
  374. data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
  375. data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
  376. data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
  377. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
  378. data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
  379. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
  380. data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
  381. data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
  382. data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
  383. data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
  384. data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
  385. data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
  386. data/lib/seams/cli/list.rb +111 -0
  387. data/lib/seams/cli/quality.rb +99 -0
  388. data/lib/seams/cli/resolve.rb +276 -0
  389. data/lib/seams/cli/test_changed.rb +116 -0
  390. data/lib/seams/cli.rb +48 -0
  391. data/lib/seams/configuration.rb +19 -0
  392. data/lib/seams/cops/known_queue_names.rb +42 -0
  393. data/lib/seams/cops/migration_comments.rb +68 -0
  394. data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
  395. data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
  396. data/lib/seams/cops.rb +18 -0
  397. data/lib/seams/event_registry.rb +49 -0
  398. data/lib/seams/events/adapter.rb +24 -0
  399. data/lib/seams/events/adapters/active_support.rb +31 -0
  400. data/lib/seams/events/publisher.rb +178 -0
  401. data/lib/seams/events.rb +39 -0
  402. data/lib/seams/generators/dummy_app_writer.rb +424 -0
  403. data/lib/seams/generators/eject_aware.rb +102 -0
  404. data/lib/seams/generators/follow_up_generator.rb +148 -0
  405. data/lib/seams/generators/host_injector.rb +124 -0
  406. data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
  407. data/lib/seams/generators/splicer.rb +217 -0
  408. data/lib/seams/observability/adapter.rb +33 -0
  409. data/lib/seams/observability/adapters/rails_logger.rb +59 -0
  410. data/lib/seams/observability.rb +34 -0
  411. data/lib/seams/runtime.rb +23 -0
  412. data/lib/seams/version.rb +5 -0
  413. data/lib/seams.rb +23 -0
  414. 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ class ApplicationJob < ::ApplicationJob
5
+ end
6
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ 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