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,266 @@
1
+ # Seams Admin
2
+
3
+ > Mountable admin engine for a Seams-powered host. Built on
4
+ > [Administrate](https://github.com/thoughtbot/administrate) for the
5
+ > dashboard surface and [Pundit](https://github.com/varvet/pundit) for
6
+ > per-record authorisation. Read-only over the existing seams tables —
7
+ > ships no migrations of its own.
8
+
9
+ ## 30-second elevator pitch
10
+
11
+ `bin/rails generate seams:admin` writes an `engines/admin/` engine into
12
+ your host that mounts at `/admin` and ships:
13
+
14
+ - Twelve Administrate dashboards covering the canonical seams models
15
+ (Identity, Account, Membership × 2, Team, TeamMembership, Invitation,
16
+ Notification, NotificationPreference, Plan, Subscription, Invoice,
17
+ LifetimePass).
18
+ - Two operating modes — **platform** (admins see everything) and
19
+ **tenant** (admins are scoped to their own Account) — selected by a
20
+ single config knob.
21
+ - Pundit policies under `Admin::Platform::*` and `Admin::Tenant::*`,
22
+ selected at request time by `Seams::Admin.config.tenancy_scope`.
23
+ - Audit-log auto-write — every successful create/update/destroy emits a
24
+ `Core::AuditLog` row keyed on `Auth::Current.identity`.
25
+ - Four config knobs (`authenticator`, `tenancy_scope`, `theme_css_path`,
26
+ `before_admin_action`) for the predictable customisation surface.
27
+
28
+ No migrations, no new tables, no admin-only User model — see
29
+ "[Why `staff?` on Identity, not a separate `AdminUser` table?](#why-staff-on-identity-not-a-separate-adminuser-table)"
30
+ below.
31
+
32
+ ## First sign-in (do this first)
33
+
34
+ After `bundle install` picks up `administrate` + `pundit`, promote
35
+ yourself to a platform admin from the Rails console (or via runner):
36
+
37
+ ```ruby
38
+ Auth::Identity.find_by(email: "you@example.com").update!(staff: true)
39
+ ```
40
+
41
+ Then boot the host and visit `/admin`:
42
+
43
+ ```bash
44
+ bin/rails server
45
+ open http://localhost:3000/admin
46
+ ```
47
+
48
+ You should land on the Identities index with sidebar entries for all
49
+ twelve canonical models. If you see "Access denied" instead, the
50
+ authenticator returned a falsey value — double-check `staff` is set on
51
+ your Identity row (`Auth::Identity.find_by(email: ...).staff?` should
52
+ return `true`).
53
+
54
+ ## Two-mode operation
55
+
56
+ The admin surface supports two tenancy modes, switched via a single
57
+ config knob:
58
+
59
+ ### `:platform` mode (default)
60
+
61
+ ```ruby
62
+ # config/initializers/seams_admin.rb
63
+ Seams::Admin.configure { |c| c.tenancy_scope = :platform }
64
+ ```
65
+
66
+ - **Gate:** `Auth::Identity#staff?` (boolean column on
67
+ `auth_identities`).
68
+ - **Visibility:** every Account's data, unfiltered.
69
+ - **Use case:** internal operator console — support staff
70
+ impersonation, cross-account search, billing reconciliation.
71
+ - **Policies:** `Admin::Platform::*Policy` namespace.
72
+
73
+ ### `:tenant` mode
74
+
75
+ ```ruby
76
+ # config/initializers/seams_admin.rb
77
+ Seams::Admin.configure { |c| c.tenancy_scope = :tenant }
78
+ ```
79
+
80
+ - **Gate:** `Accounts::Membership#role == "admin"` (read off
81
+ `Accounts::Current.membership`).
82
+ - **Visibility:** only the current Account's data — every Pundit
83
+ `Scope` filters by `account_id`.
84
+ - **Use case:** a tenant-facing admin surface where each customer's
85
+ admin sees their own users / invoices / teams only.
86
+ - **Policies:** `Admin::Tenant::*Policy` namespace.
87
+ - **Requires:** the `accounts` engine. Without it the admin engine
88
+ fails fast at boot with a clear error message.
89
+
90
+ Switching modes is a config flip — no schema change, no regenerate.
91
+ The `policy_namespace` lookup on `Seams::Admin::ApplicationController`
92
+ reads `Seams::Admin.config.tenancy_scope` on every request.
93
+
94
+ ## Configuration
95
+
96
+ Four knobs, deliberately small:
97
+
98
+ ```ruby
99
+ # config/initializers/seams_admin.rb
100
+ Seams::Admin.configure do |c|
101
+ # 1. authenticator — Callable taking the controller, returning truthy
102
+ # when the request is allowed in. Default: ->(ctrl) { ctrl.current_identity&.staff? }
103
+ c.authenticator = ->(ctrl) { ctrl.current_identity&.staff? }
104
+
105
+ # 2. tenancy_scope — :platform | :tenant. Selects the policy namespace
106
+ # at request time. See "Two-mode operation" above.
107
+ c.tenancy_scope = :platform
108
+
109
+ # 3. theme_css_path — Optional path to a host-supplied CSS file loaded
110
+ # on top of Administrate's stock styling. nil = use Administrate's
111
+ # defaults.
112
+ c.theme_css_path = nil
113
+
114
+ # 4. before_admin_action — Optional callable that runs before every
115
+ # admin action. The recommended hook for 2FA enforcement, IP
116
+ # allow-listing, audit-trail entry-point logging, etc. Receives the
117
+ # controller; raise to halt the request.
118
+ c.before_admin_action = nil
119
+ end
120
+ ```
121
+
122
+ Follow-up generators that ship new knobs splice their `attr_accessor`
123
+ into the `admin.configuration.attributes` insertion point in
124
+ `engines/admin/lib/admin/configuration.rb` and the matching default
125
+ into `admin.configuration.defaults`.
126
+
127
+ ## Why `staff?` on Identity, not a separate `AdminUser` table?
128
+
129
+ Classic Rails apps separate `User` and `AdminUser` because `User`
130
+ carries customer-facing concerns (orders, billing, notifications, etc.)
131
+ that you do not want admins to inherit. **In seams Wave 9
132
+ `Auth::Identity` is *already* the credential-only object** — it has no
133
+ customer concerns to inherit from. Customer-facing data lives on
134
+ `Accounts::Membership` (account-scoped) and the host's own domain
135
+ models. A boolean `staff?` flag on Identity is the right granularity.
136
+
137
+ The seams "User and AdminUser must live on separate tables" rule is
138
+ **reinterpreted, not violated**: the rule's intent (no customer state
139
+ on the admin authentication object) is fully satisfied by the Wave 9
140
+ credential-only Identity. Flagging it explicitly here so future
141
+ contributors don't read this engine and assume the rule was forgotten.
142
+
143
+ Hosts that need hard isolation (regulatory compliance, separate auth
144
+ flow, dedicated SSO for admins) override the authenticator:
145
+
146
+ ```ruby
147
+ Seams::Admin.configure do |c|
148
+ c.authenticator = ->(ctrl) { ctrl.current_admin_user.present? }
149
+ end
150
+ ```
151
+
152
+ …and bring their own `current_admin_user` resolution from a separate
153
+ auth surface.
154
+
155
+ ## Customising a dashboard (Ejecting)
156
+
157
+ The seams pattern: **eject the dashboard, then edit your local copy**.
158
+ Future `bin/seams admin` runs leave ejected files alone.
159
+
160
+ ```bash
161
+ bin/seams resolve --eject admin/app/dashboards/admin/identity_dashboard.rb
162
+ # Now edit engines/admin/app/dashboards/admin/identity_dashboard.rb freely.
163
+ ```
164
+
165
+ For new dashboards covering host-specific models, generate via
166
+ Administrate, then move the result under the `Admin::*` namespace:
167
+
168
+ ```bash
169
+ bin/rails generate administrate:dashboard MyHost::Project
170
+ # Move app/dashboards/my_host/project_dashboard.rb to
171
+ # engines/admin/app/dashboards/admin/project_dashboard.rb
172
+ # Move the controller similarly under app/controllers/admin/.
173
+ # Add `resources :projects, controller: "admin/projects"` to
174
+ # engines/admin/config/routes.rb (or splice via the
175
+ # admin.routes.after_resources insertion point in a follow-up generator).
176
+ ```
177
+
178
+ Administrate's full dashboard / field / controller surface is documented
179
+ at <https://administrate-demo.herokuapp.com/>.
180
+
181
+ ## Error messages a host operator will see
182
+
183
+ | Symptom | Likely cause | Fix |
184
+ | --- | --- | --- |
185
+ | `[seams admin] missing required dependency: Auth::Identity ...` at boot | The auth engine isn't installed. | `bin/rails generate seams:auth` |
186
+ | `[seams admin] missing required dependency: Administrate ...` at boot | `gem "administrate"` not in the host Gemfile. | `bundle install` (the admin generator wires it; if you regenerated and skipped, add `gem "administrate", "~> 1.0"` manually). |
187
+ | `[seams admin] missing required dependency: Pundit ...` at boot | `gem "pundit"` not in the host Gemfile. | Same fix — the generator wires it. |
188
+ | 403 "Access denied. The seams admin surface is gated on `Seams::Admin.config.authenticator`..." on every request | The configured gate returned falsey for the signed-in Identity. | Default gate is `current_identity&.staff?`. Set `staff: true` on your Identity row, or override the authenticator. |
189
+ | `Pundit::NotAuthorizedError` mid-request | A per-action policy denied the request. | Eject the relevant policy under `engines/admin/app/policies/admin/{platform,tenant}/<resource>_policy.rb` and override the action predicate (e.g. `def destroy?; false; end`). |
190
+
191
+ ## What's not shipped (deferred)
192
+
193
+ - **2FA / IP allow-list for `/admin`.** Not shipped. Wire your own via
194
+ `Seams::Admin.config.before_admin_action`.
195
+ - **Branded UI.** Stock Administrate CSS only in v1. Override via
196
+ `theme_css_path`.
197
+ - **Search beyond Administrate's stock.** Administrate ships a basic
198
+ Ransack-backed search; rich filtering / faceted search is your job.
199
+ - **Batch actions.** Not in Administrate's surface; not shipped here.
200
+ Override the relevant controller's `index` action and emit your own
201
+ bulk-action button if you need them.
202
+
203
+ ## Insertion-point markers
204
+
205
+ This engine ships five Wave-10 insertion-point markers that follow-up
206
+ generators target:
207
+
208
+ | Marker | File | Purpose |
209
+ | --- | --- | --- |
210
+ | `admin.engine.events` | `lib/admin/engine.rb` | Register new admin events (e.g. `admin.action.taken.admin`). |
211
+ | `admin.routes.before_resources` | `config/routes.rb` | Ahead-of-resource routes (impersonation entry points, bulk-action endpoints). |
212
+ | `admin.routes.after_resources` | `config/routes.rb` | New top-level admin routes (custom collection routes, JSON-only endpoints). |
213
+ | `admin.configuration.attributes` | `lib/admin/configuration.rb` | New configuration knobs. |
214
+ | `admin.configuration.defaults` | `lib/admin/configuration.rb` | Defaults for the new knobs. |
215
+
216
+ `bin/seams resolve --list-markers admin` prints the live list against
217
+ the engine source.
218
+
219
+ ## Audit log
220
+
221
+ Every successful create/update/destroy on an admin dashboard writes a
222
+ `Core::AuditLog` row via the `record_admin_audit` after_action. The
223
+ row carries:
224
+
225
+ - `action` — `"create"`, `"update"`, or `"destroy"`.
226
+ - `auditable_type` / `auditable_id` — the resource Administrate
227
+ resolved.
228
+ - `actor_id` — `Auth::Current.identity&.id`.
229
+ - `payload` — for updates, `record.saved_changes.transform_values(&:last)`;
230
+ for create/destroy, attributes minus timestamps.
231
+
232
+ If the core engine isn't installed (and `Core::AuditLog` is therefore
233
+ undefined), the after_action no-ops cleanly — admin still works,
234
+ audit log is silently absent.
235
+
236
+ ## Why Administrate?
237
+
238
+ Considered: ActiveAdmin, Avo, Trestle, Motor Admin, RailsAdmin, and
239
+ hand-rolling on top of Rails 8 scaffolds. Administrate wins because:
240
+
241
+ - Dashboards are plain Ruby classes (`Admin::IdentityDashboard <
242
+ Administrate::BaseDashboard`). No DSL, no Arbre. Matches seams' house
243
+ style.
244
+ - Custom fields are `rails g administrate:field foo` — a class + an ERB
245
+ partial. Perfect for masking encrypted columns and rendering Stripe
246
+ IDs.
247
+ - ORM-agnostic.
248
+ - Pundit-aware via documented `policy_namespace` integration. Clean
249
+ surface for the platform-vs-tenant policy split.
250
+ - thoughtbot maintains it.
251
+ - Easiest of the surveyed frameworks to embed inside another engine —
252
+ no assumption that the host owns `/admin`.
253
+
254
+ The full per-framework comparison lives in
255
+ `proposals/admin_engine_administrate.md` (the Wave 11A research
256
+ report).
257
+
258
+ ## Running the specs
259
+
260
+ ```bash
261
+ bin/rails seams:test[admin]
262
+ ```
263
+
264
+ The boot spec asserts the engine loads, the four config knobs default
265
+ correctly, every dashboard + policy class is reachable, and both
266
+ tenancy modes route to the right policy namespace.
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Accounts::Account dashboard.
4
+ # Subclasses Seams::Admin::ApplicationController so the gate, the
5
+ # pundit_user hook, and Phase 3's audit-log auto-write apply.
6
+ module Admin
7
+ class AccountsController < ::Seams::Admin::ApplicationController
8
+ def resource_class
9
+ ::Accounts::Account
10
+ end
11
+
12
+ def dashboard
13
+ @dashboard ||= AccountDashboard.new
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Accounts::Membership dashboard.
4
+ # Routed under `accounts_memberships` (engine-prefixed) to disambiguate
5
+ # from `teams_memberships` — both engines ship a Membership model.
6
+ module Admin
7
+ class AccountsMembershipsController < ::Seams::Admin::ApplicationController
8
+ def resource_class
9
+ ::Accounts::Membership
10
+ end
11
+
12
+ def dashboard
13
+ @dashboard ||= AccountsMembershipDashboard.new
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ module Admin
5
+ # Base controller for the seams admin engine. Subclasses
6
+ # Administrate::ApplicationController so dashboards (Phase 2) get
7
+ # the framework's defaults — index/show/new/create/edit/update/destroy,
8
+ # search, filtering, pagination — for free.
9
+ #
10
+ # ## Three layered responsibilities
11
+ #
12
+ # 1. **Authentication gate** — Phase 1's `Seams::Admin::Authenticator`
13
+ # concern installs `before_action :authenticate_admin!`. Default
14
+ # gate: `current_identity&.staff?`. Configurable via
15
+ # `Seams::Admin.config.authenticator`.
16
+ #
17
+ # 2. **Resource-level authorisation** — Phase 3 wires Pundit via
18
+ # Administrate's canonical `Administrate::Punditize` concern.
19
+ # `Punditize` is the integration thoughtbot ships and documents:
20
+ # it overrides Administrate's `authorized_action?` and
21
+ # `scoped_resource` to delegate to Pundit, so a per-action
22
+ # permission check (`policy.update?`) AND the list-view
23
+ # `policy_scope` both fire automatically. Without including
24
+ # `Punditize`, Administrate's default `authorized_action?`
25
+ # returns true for everything — Pundit policies would be loaded
26
+ # but never consulted.
27
+ #
28
+ # The platform-vs-tenant split lives in `policy_namespace`:
29
+ # Pundit's array-form lookup (`Pundit.policy!(user, [ns, record])`)
30
+ # resolves to `Admin::Platform::*Policy` or
31
+ # `Admin::Tenant::*Policy` depending on the active mode.
32
+ #
33
+ # 3. **Audit-log auto-write** — Phase 3 emits a `Core::AuditLog` row
34
+ # for every successful create/update/destroy via the
35
+ # `record_admin_audit` after_action. The actor is
36
+ # `Auth::Current.identity` (the human driving the request); the
37
+ # auditable is Administrate's `requested_resource`.
38
+ #
39
+ # The order here is deliberate: gate, then authorise, then act,
40
+ # then audit. A request that fails the gate never reaches a
41
+ # policy; a request that fails a policy never reaches the action;
42
+ # the audit row only writes after a successful action.
43
+ class ApplicationController < ::Administrate::ApplicationController
44
+ include ::Seams::Admin::Authenticator
45
+ # Administrate's canonical Pundit integration. Internally:
46
+ # - includes Pundit::Authorization
47
+ # - overrides `authorized_action?` to call
48
+ # `Pundit.policy!(pundit_user, [*policy_namespace, resource]).public_send("#{action}?")`
49
+ # - overrides `scoped_resource` to apply `Pundit.policy_scope!`
50
+ # using the same array-form namespace
51
+ # - overrides `authorize_resource` to use `authorize` with the
52
+ # namespaced array
53
+ # Defining our own `policy_namespace` is the supported way to
54
+ # switch between Admin::Platform::* and Admin::Tenant::*.
55
+ include ::Administrate::Punditize
56
+
57
+ # Pundit raises `Pundit::NotAuthorizedError` when a policy denies a
58
+ # request. Without an explicit rescue the host inherits the
59
+ # exception and renders a 500. Treat it as a 403 and surface a
60
+ # clear message; the after_action audit hook also runs (response
61
+ # is set), so audit_log records the rejected attempt.
62
+ rescue_from ::Pundit::NotAuthorizedError, with: :respond_with_admin_unauthorised
63
+
64
+ # Catch the parallel exception Administrate raises from inside its
65
+ # own `authorize_resource` when (for whatever reason) Punditize is
66
+ # bypassed. Same handling — present the operator with a 403.
67
+ rescue_from ::Administrate::NotAuthorizedError, with: :respond_with_admin_unauthorised
68
+
69
+ # Phase 3 audit-log auto-write. Runs after successful create /
70
+ # update / destroy actions. Wrapped in `if defined?(Core::AuditLog)`
71
+ # so the engine is usable without the seams core engine —
72
+ # operators who don't ship core get no audit log, which is the
73
+ # correct behaviour when the table doesn't exist.
74
+ after_action :record_admin_audit, only: %i[create update destroy]
75
+
76
+ # Pundit ships `verify_authorized` / `verify_policy_scoped`
77
+ # after_actions that raise if a controller didn't call
78
+ # `authorize` / `policy_scope`. Administrate's `Punditize`
79
+ # already calls them on every standard action, so any future
80
+ # custom action added to a subclass that forgets the call gets
81
+ # caught loudly in development. `:only` excludes `:index`
82
+ # (covered by `verify_policy_scoped` instead) and the rescue
83
+ # responders themselves (which never authorise — they 403).
84
+ after_action :verify_authorized, except: %i[index respond_with_admin_unauthorised]
85
+ after_action :verify_policy_scoped, only: %i[index]
86
+
87
+ # Helper exposed to dashboards so they can read the active
88
+ # tenancy mode (:platform vs :tenant). Phase 3 reads this in
89
+ # policy scopes; dashboards may also branch on it for
90
+ # account_id columns.
91
+ helper_method :seams_admin_tenancy_scope
92
+
93
+ def seams_admin_tenancy_scope
94
+ ::Seams::Admin.config.tenancy_scope
95
+ end
96
+
97
+ # Pundit hook. Returns a `Seams::Admin::Context` Struct wrapping
98
+ # the current Identity and the current Accounts::Membership so
99
+ # tenant policies can read both signals (staff? + role/account_id)
100
+ # without each policy reaching into the controller. Returning a
101
+ # raw Identity would lose the membership; returning the
102
+ # controller would expose too much. The Struct is the smallest
103
+ # surface that lets every policy stay decoupled from request
104
+ # state.
105
+ #
106
+ # `current_membership` is read defensively: hosts that don't
107
+ # ship the accounts engine (platform-only deployments) leave it
108
+ # nil, and the platform policies don't need it.
109
+ def pundit_user
110
+ identity = ::Auth::Current.identity if defined?(::Auth::Current)
111
+ membership = current_membership_for_admin
112
+ ::Seams::Admin::Context.new(identity, membership)
113
+ end
114
+
115
+ # The Pundit policy namespace for the active tenancy mode.
116
+ # `Administrate::Punditize` calls `Pundit.policy!(pundit_user,
117
+ # [*policy_namespace, resource])` — Pundit's array-form lookup
118
+ # walks the namespace tree (`Admin::Platform::IdentityPolicy`
119
+ # for an `Auth::Identity` resource under `[Admin, Platform]`).
120
+ # Returning an Array of constants (NOT a single Module) is what
121
+ # Punditize expects.
122
+ #
123
+ # The `Admin` segment is the engine's policy module root; the
124
+ # second segment switches on tenancy mode. Hosts switching modes
125
+ # at runtime per-request (rare; typically configured once at boot)
126
+ # override this method on a subclass and consult their own request
127
+ # state.
128
+ def policy_namespace
129
+ case ::Seams::Admin.config.tenancy_scope
130
+ when :tenant then [::Admin::Tenant]
131
+ else [::Admin::Platform]
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ # Resolve the current Accounts::Membership for tenant-mode
138
+ # policy decisions. Delegates to
139
+ # `Seams::Admin.config.current_membership_resolver` (a callable
140
+ # taking the controller). The default reads
141
+ # `Accounts::Current.membership` (Wave 9's CurrentAttributes
142
+ # object). Hosts using a different membership-resolution shape
143
+ # set their own resolver in `config/initializers/seams_admin.rb`
144
+ # rather than ejecting this controller.
145
+ def current_membership_for_admin
146
+ resolver = ::Seams::Admin.config.current_membership_resolver
147
+ return nil unless resolver.respond_to?(:call)
148
+
149
+ resolver.call(self)
150
+ rescue StandardError => e
151
+ Rails.logger.warn("[seams admin] membership resolver raised: #{e.class}: #{e.message}") if defined?(Rails)
152
+ nil
153
+ end
154
+
155
+ # Phase 3 audit-log auto-write. Records a `Core::AuditLog` row
156
+ # for every successful create/update/destroy.
157
+ #
158
+ # Three guards keep the row from writing in cases that would
159
+ # either crash or duplicate:
160
+ #
161
+ # - `defined?(::Core::AuditLog)` — the engine is usable
162
+ # without the seams core engine; no constant means no row.
163
+ # - `response.successful? || response.redirect?` — only audit
164
+ # actions whose response is in the 2xx/3xx range. A failed
165
+ # update (4xx) skipped through the after_action shouldn't
166
+ # leave a misleading "update happened" row.
167
+ # - `record.respond_to?(:audit_logs)` — when the host model
168
+ # already includes `Core::Auditable`, that concern's
169
+ # `after_commit` hook ALREADY wrote a row. Writing a second
170
+ # row from this controller would double-log. We detect the
171
+ # `audit_logs` association the concern installs and skip.
172
+ #
173
+ # `requested_resource` is Administrate's resolved record. On
174
+ # `create` it's the just-saved record; on `update` it's the
175
+ # just-updated record; on `destroy` it's the record that was just
176
+ # destroyed (frozen but still readable for `id` / `class.name`).
177
+ def record_admin_audit
178
+ return unless defined?(::Core::AuditLog)
179
+ return unless response.successful? || response.redirect?
180
+
181
+ record = requested_resource_for_audit
182
+ return if record.nil?
183
+ return if auditable_via_concern?(record)
184
+
185
+ ::Core::AuditLog.create!(
186
+ action: action_name,
187
+ auditable_type: record.class.name,
188
+ auditable_id: record.id,
189
+ actor_id: admin_audit_actor_id,
190
+ payload: admin_audit_payload(record)
191
+ )
192
+ rescue StandardError => e
193
+ # Audit-log failures must not break the admin response. A
194
+ # missing column, a transient DB hiccup, or a unique-constraint
195
+ # violation should be logged and swallowed — the admin action
196
+ # itself already succeeded by the time after_action runs.
197
+ # Operators wanting a structured signal can subscribe to
198
+ # ActiveSupport::Notifications "seams.admin.audit_failed".
199
+ Rails.logger.warn("[seams admin] audit log write failed: #{e.class}: #{e.message}") if defined?(Rails)
200
+ if defined?(::ActiveSupport::Notifications)
201
+ ::ActiveSupport::Notifications.instrument(
202
+ "seams.admin.audit_failed",
203
+ error_class: e.class.name, message: e.message,
204
+ action: action_name, resource_class: record&.class&.name
205
+ )
206
+ end
207
+ end
208
+
209
+ # Read `requested_resource` defensively. On a destroy action
210
+ # Administrate's `requested_resource` is the just-destroyed
211
+ # (frozen) record; reading `.id` and `.class.name` still works.
212
+ # On other actions Administrate caches the resource in
213
+ # `@requested_resource`, so calling the public method is cheap.
214
+ # Wrap in a rescue: if the action never resolved a resource (a
215
+ # subclass that overrode `requested_resource` to raise) we don't
216
+ # want the audit hook to take down the whole response.
217
+ def requested_resource_for_audit
218
+ respond_to?(:requested_resource, true) ? requested_resource : nil
219
+ rescue StandardError
220
+ nil
221
+ end
222
+
223
+ # `Core::Auditable` adds `has_many :audit_logs` to the host model
224
+ # AND wires `after_commit` callbacks that write a row directly.
225
+ # If the model includes the concern, the row was already written
226
+ # by the model's own callback — writing another from here would
227
+ # double-log. The `audit_logs` association is the cheapest
228
+ # detection signal that doesn't require requiring `Core::Auditable`
229
+ # itself (which lives in the core engine, not loaded by the
230
+ # admin engine's dummy app).
231
+ def auditable_via_concern?(record)
232
+ return false unless record.class.respond_to?(:reflect_on_association)
233
+
234
+ reflection = record.class.reflect_on_association(:audit_logs)
235
+ return false if reflection.nil?
236
+
237
+ reflection.options[:as] == :auditable
238
+ end
239
+
240
+ def admin_audit_actor_id
241
+ return nil unless defined?(::Auth::Current)
242
+
243
+ ::Auth::Current.identity&.id
244
+ end
245
+
246
+ def admin_audit_payload(record)
247
+ case action_name
248
+ when "update"
249
+ # `saved_changes` is the canonical Rails surface for "what
250
+ # this update actually wrote", available on the just-saved
251
+ # record. Empty for a no-op update.
252
+ record.respond_to?(:saved_changes) ? record.saved_changes.transform_values(&:last) : {}
253
+ else
254
+ # create + destroy snapshot the attributes minus timestamps
255
+ # so the row is readable without the audit-log consumer
256
+ # having to filter timestamp noise.
257
+ record.respond_to?(:attributes) ? record.attributes.except("created_at", "updated_at") : {}
258
+ end
259
+ end
260
+
261
+ # Pundit / Administrate authorisation failure handler. Renders a
262
+ # plain 403 — same shape as the authenticator concern's
263
+ # `respond_with_admin_forbidden` to keep the operator-visible
264
+ # surface consistent.
265
+ def respond_with_admin_unauthorised(_exception = nil)
266
+ message = <<~MSG.strip
267
+ Access denied. The seams admin engine's Pundit policy denied
268
+ this request. The active tenancy mode is
269
+ `Seams::Admin.config.tenancy_scope = #{::Seams::Admin.config.tenancy_scope.inspect}`;
270
+ if you expected access, check the matching policy under
271
+ `Admin::#{::Seams::Admin.config.tenancy_scope.to_s.camelize}::*Policy`.
272
+ MSG
273
+
274
+ if respond_to?(:render)
275
+ render plain: message, status: :forbidden
276
+ else
277
+ head :forbidden
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Auth::Identity dashboard.
4
+ #
5
+ # Subclasses Seams::Admin::ApplicationController (Phase 1's
6
+ # gate-bearing parent) so the authenticator concern, the
7
+ # `pundit_user` hook, and Phase 3's audit-log auto-write apply
8
+ # uniformly across every dashboard. Do NOT inherit directly from
9
+ # Administrate::ApplicationController — that bypasses the gate.
10
+ #
11
+ # Administrate resolves `resource_class` from the controller name
12
+ # automatically via its inflector hook, but we override it explicitly
13
+ # here because the Identity model lives under the Auth namespace
14
+ # (Auth::Identity) and the default inflection would resolve to a
15
+ # top-level Identity constant that does not exist.
16
+ module Admin
17
+ class IdentitiesController < ::Seams::Admin::ApplicationController
18
+ def resource_class
19
+ ::Auth::Identity
20
+ end
21
+
22
+ def dashboard
23
+ @dashboard ||= IdentityDashboard.new
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Teams::Invitation dashboard.
4
+ module Admin
5
+ class InvitationsController < ::Seams::Admin::ApplicationController
6
+ def resource_class
7
+ ::Teams::Invitation
8
+ end
9
+
10
+ def dashboard
11
+ @dashboard ||= InvitationDashboard.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Billing::Invoice dashboard.
4
+ module Admin
5
+ class InvoicesController < ::Seams::Admin::ApplicationController
6
+ def resource_class
7
+ ::Billing::Invoice
8
+ end
9
+
10
+ def dashboard
11
+ @dashboard ||= InvoiceDashboard.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the Billing::LifetimePass dashboard.
4
+ module Admin
5
+ class LifetimePassesController < ::Seams::Admin::ApplicationController
6
+ def resource_class
7
+ ::Billing::LifetimePass
8
+ end
9
+
10
+ def dashboard
11
+ @dashboard ||= LifetimePassDashboard.new
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Administrate controller for the
4
+ # Notifications::NotificationPreference dashboard.
5
+ module Admin
6
+ class NotificationPreferencesController < ::Seams::Admin::ApplicationController
7
+ def resource_class
8
+ ::Notifications::NotificationPreference
9
+ end
10
+
11
+ def dashboard
12
+ @dashboard ||= NotificationPreferenceDashboard.new
13
+ end
14
+ end
15
+ end