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,289 @@
1
+ # Auth
2
+
3
+ > Email + password authentication for a Seams-powered host application.
4
+ > The canonical "human" record is `Auth::Identity` (post-Wave-9). Other
5
+ > engines address the human via Identity, not via a host User.
6
+
7
+ ## Events emitted
8
+
9
+ | Event name | Payload | Emitted when |
10
+ | --- | --- | --- |
11
+ | `identity.signed_up.auth` | `{ identity_id:, email: }` | RegistrationsController#create succeeds |
12
+ | `identity.signed_in.auth` | `{ identity_id:, session_id: }` | SessionsController#create succeeds |
13
+ | `identity.signed_out.auth` | `{ identity_id:, session_id: }` | SessionsController#destroy runs |
14
+ | `session.expired.auth` | `{ identity_id:, session_id: }` | sweeper job revokes a stale session |
15
+
16
+ > `identity_id` is the row id in the engine's `auth_identities` table.
17
+ > Subscribers that resolve identities should use `identity_id`.
18
+
19
+ ## Events consumed
20
+
21
+ This engine does not subscribe to any other engine's events.
22
+
23
+ ## Exposed concerns
24
+
25
+ | Concern | Purpose |
26
+ | --- | --- |
27
+ | `Auth::Authenticatable` | Optional post-Wave-9. Mix into a host User (if you keep one) to add session-aware helpers (`signed_in?`, `sign_out_everywhere!`); the host model needs an `identity_id` column. Most hosts skip this — `Auth::Identity` is the canonical human. |
28
+ | `Auth::Authentication` | Mix into the host's ApplicationController to get `current_identity`, `signed_in?`, `authenticate_identity!` plus a per-request `Auth::Current.identity`. |
29
+
30
+ ## Adapters
31
+
32
+ Password hashing is provided by `bcrypt` via Rails' `has_secure_password`.
33
+ To swap in a different hasher, override `Auth::Identity`'s
34
+ `password_digest=` setter in your host application.
35
+
36
+ ## Password reset (Rails 8 has_secure_password reset_token)
37
+
38
+ `Auth::Identity` uses Rails 8's built-in reset token machinery — a
39
+ signed_id with a 15-minute default expiry, generated on demand,
40
+ verified via `find_by_password_reset_token`. **No `password_reset_token`
41
+ column, no `password_reset_token_sent_at`, no sweep job.** Token
42
+ expiry is a property of the signature, not a database row.
43
+
44
+ To configure the expiry:
45
+
46
+ ```ruby
47
+ class Auth::Identity < ApplicationRecord
48
+ has_secure_password reset_token: { expires_in: 30.minutes }
49
+ end
50
+ ```
51
+
52
+ ## OAuth (Sign in with Google / GitHub)
53
+
54
+ Two adapters ship with the engine — `Auth::OAuth::Google` and
55
+ `Auth::OAuth::Github` — both built on Faraday. **No `oauth2` gem
56
+ dependency**, no `Net::HTTP`. Adapter contract lives in
57
+ `lib/auth/oauth/abstract.rb`; subclass it for additional providers
58
+ (Apple, GitLab, Microsoft, etc.).
59
+
60
+ ### Configure
61
+
62
+ ```ruby
63
+ # config/initializers/auth.rb
64
+ Auth.configure do |c|
65
+ c.oauth_providers = {
66
+ google: {
67
+ adapter: "Auth::OAuth::Google",
68
+ client_id: ENV.fetch("GOOGLE_OAUTH_CLIENT_ID"),
69
+ client_secret: ENV.fetch("GOOGLE_OAUTH_CLIENT_SECRET")
70
+ },
71
+ github: {
72
+ adapter: "Auth::OAuth::Github",
73
+ client_id: ENV.fetch("GITHUB_OAUTH_CLIENT_ID"),
74
+ client_secret: ENV.fetch("GITHUB_OAUTH_CLIENT_SECRET")
75
+ }
76
+ }
77
+ end
78
+ ```
79
+
80
+ The provider's redirect URI must match the URL Rails generates for
81
+ `auth.oauth_callback_url(provider: :google)` (e.g.
82
+ `https://your-app.com/auth/oauth/google/callback`).
83
+
84
+ ### Token storage + encryption
85
+
86
+ `Auth::OAuth::Provider` rows store `access_token` + `refresh_token`
87
+ encrypted at the column level via Rails 7+ ActiveRecord::Encryption.
88
+ **One-time host setup:**
89
+
90
+ ```bash
91
+ bin/rails db:encryption:init
92
+ ```
93
+
94
+ This prints three keys (primary, deterministic, key derivation salt).
95
+ Store them in Rails credentials (`bin/rails credentials:edit`) under
96
+ `active_record_encryption.*` — see
97
+ https://guides.rubyonrails.org/active_record_encryption.html.
98
+
99
+ ### Render the sign-in buttons
100
+
101
+ Drop the partial into your sessions or registrations form:
102
+
103
+ ```erb
104
+ <%%= render "auth/sessions/oauth_buttons" %>
105
+ ```
106
+
107
+ It iterates `Auth.configuration.oauth_providers` and renders one
108
+ `auth.oauth_start_path(provider:)` link per configured provider.
109
+
110
+ ### Routes
111
+
112
+ ```
113
+ GET /auth/oauth/:provider/start → redirect to provider's authorize URL
114
+ GET /auth/oauth/:provider/callback → exchange code, sign in, set cookie
115
+ ```
116
+
117
+ Verified against:
118
+ - https://developers.google.com/identity/protocols/oauth2/web-server
119
+ - https://developers.google.com/identity/openid-connect/openid-connect
120
+ - https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
121
+ - https://docs.github.com/en/rest/users/users
122
+ - https://docs.github.com/en/rest/users/emails
123
+
124
+ ## API tokens (Bearer auth)
125
+
126
+ `Auth::ApiToken` ships with the engine for programmatic access. Tokens
127
+ are issued via `Auth::GenerateApiToken.call(identity:, name:, expires_at:)`
128
+ which returns a `Result` carrying both the persisted record and the
129
+ **plaintext token** — the plaintext is shown ONCE and never recoverable
130
+ from the DB (only a SHA-256 digest is stored).
131
+
132
+ ```ruby
133
+ result = Auth::GenerateApiToken.call(identity: current_identity, name: "CI key")
134
+ result.plaintext # => "seam_tF9...xQ" — display once, then discard
135
+ result.api_token # => Auth::ApiToken row
136
+ ```
137
+
138
+ Mix `Auth::ApiAuthenticatable` into API controllers and call
139
+ `authenticate_api_token!` in a before_action:
140
+
141
+ ```ruby
142
+ class Api::WidgetsController < ApplicationController
143
+ include Auth::ApiAuthenticatable
144
+ before_action :authenticate_api_token!
145
+ end
146
+ ```
147
+
148
+ Clients send `Authorization: Bearer seam_<token>`. On success the
149
+ concern sets `current_identity` + `current_api_token` and bumps the
150
+ token's `last_used_at`. On failure it renders 401 JSON.
151
+
152
+ Events emitted:
153
+
154
+ | Event name | Payload |
155
+ | --- | --- |
156
+ | `api_token.issued.auth` | `{ identity_id:, api_token_id:, token_prefix: }` |
157
+ | `api_token.revoked.auth` | `{ identity_id:, api_token_id:, token_prefix: }` |
158
+
159
+ ## Rate limiting
160
+
161
+ The engine uses Rails 8's built-in `rate_limit` (backed by Solid Cache):
162
+
163
+ | Controller | Action | Limit |
164
+ | --- | --- | --- |
165
+ | `SessionsController` | `create` | 10 / minute |
166
+ | `RegistrationsController` | `create` | 5 / hour |
167
+ | `PasswordResetsController` | `create`, `update` | 5 / hour |
168
+
169
+ Tune by overriding the `rate_limit` declaration in your host app
170
+ controllers. Solid Cache is the default Rails 8 cache store; if your
171
+ host uses a different store, the limit applies to whatever cache backs
172
+ `Rails.cache`.
173
+
174
+ ## Cleanup expired sessions
175
+
176
+ `Auth::CleanupExpiredSessionsJob` sweeps expired `Auth::Session` rows
177
+ and emits `session.expired.auth` for each one. Wire it as a Solid Queue
178
+ Recurring entry in `config/recurring.yml`:
179
+
180
+ ```yaml
181
+ auth_cleanup_expired_sessions:
182
+ class: Auth::CleanupExpiredSessionsJob
183
+ schedule: "every 1 hour"
184
+ ```
185
+
186
+ ## Platform admin (`staff?`)
187
+
188
+ `Auth::Identity` ships with a `staff` boolean (default false) — an
189
+ Identity-level platform-admin flag that host admin tooling can use to
190
+ bypass account scoping. NOT related to per-account roles (those live
191
+ on `Accounts::Membership`, owned by the accounts engine).
192
+
193
+ ```ruby
194
+ identity.staff? # => false (default)
195
+ identity.update!(staff: true)
196
+ identity.staff? # => true
197
+ ```
198
+
199
+ Promotion is an admin operation only — `Auth::RegisterIdentity` does
200
+ NOT honour `staff:` from sign-up params.
201
+
202
+ ## GDPR / data protection
203
+
204
+ Personal data the engine stores and how it's protected at rest:
205
+
206
+ | Column | At rest | Why |
207
+ | --- | --- | --- |
208
+ | `auth_identities.email` | encrypted (deterministic) | PII (Article 4); deterministic so `find_by(email:)` and the uniqueness index keep working |
209
+ | `auth_identities.password_digest` | bcrypt one-way hash | not PII — never reversible to a password |
210
+ | `auth_oauth_providers.access_token` | encrypted (non-deterministic) | credential |
211
+ | `auth_oauth_providers.refresh_token` | encrypted (non-deterministic) | credential |
212
+ | `auth_oauth_providers.provider_uid` | encrypted (deterministic) | online identifier (Article 4); deterministic so `(provider, provider_uid)` lookup + unique index keep working |
213
+ | `auth_api_tokens.token_digest` | SHA-256 hash | not reversible to the plaintext token |
214
+ | `auth_api_tokens.token_prefix` | plaintext | first 12 chars only — display label, not a secret |
215
+
216
+ ### One-time host setup
217
+
218
+ ```bash
219
+ bin/rails db:encryption:init
220
+ ```
221
+
222
+ Store the printed keys in Rails credentials
223
+ (`bin/rails credentials:edit`) under `active_record_encryption.*`. See
224
+ https://guides.rubyonrails.org/active_record_encryption.html.
225
+
226
+ ### Upgrading from Auth Wave ≤10 (existing hosts only)
227
+
228
+ Hosts that deployed Auth before Wave 11 have plaintext `email` +
229
+ `provider_uid` already in the database. Re-encrypt them in place:
230
+
231
+ 1. In `config/application.rb`, add the transitional flag so plaintext
232
+ rows are still readable while rotation runs:
233
+
234
+ ```ruby
235
+ config.active_record.encryption.support_unencrypted_data = true
236
+ ```
237
+
238
+ 2. Deploy + run:
239
+
240
+ ```bash
241
+ bin/rails seams:auth:rotate_pii_encryption
242
+ ```
243
+
244
+ 3. Once the task reports zero remaining unencrypted rows, remove the
245
+ flag (or set it back to `false`) and redeploy.
246
+
247
+ The task is idempotent — re-running it on already-encrypted rows is a
248
+ no-op. Fresh hosts skip steps 1 and 3 entirely.
249
+
250
+ ### Right to erasure (Article 17)
251
+
252
+ ```ruby
253
+ Auth::Identity.find_by(email: "user@example.com").destroy
254
+ ```
255
+
256
+ cascades to `sessions`, `api_tokens`, and `oauth_providers` via
257
+ `dependent: :destroy`. Hosts that keep their own user-domain records
258
+ linked by `identity_id` must additionally erase those rows — the
259
+ engine does not own that schema.
260
+
261
+ ### Logging
262
+
263
+ Don't log `email`. Log `identity_id` instead. Encryption protects the
264
+ DB at rest but is moot if PII leaks into log files. Add the column to
265
+ `config.filter_parameters` in your host:
266
+
267
+ ```ruby
268
+ config.filter_parameters += %i[email password password_digest]
269
+ ```
270
+
271
+ ### Right to access / portability (Article 15 / 20)
272
+
273
+ Not yet shipped — `Auth::ExportIdentityData` is on the roadmap. For
274
+ now, hosts can export with a direct query against the identity's rows.
275
+
276
+ ## Mounting
277
+
278
+ ```ruby
279
+ # config/routes.rb (host application)
280
+ Rails.application.routes.draw do
281
+ mount Auth::Engine, at: "/auth"
282
+ end
283
+ ```
284
+
285
+ ## Running the specs
286
+
287
+ ```bash
288
+ bin/rails seams:test[auth]
289
+ ```
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module OAuth
5
+ # Two endpoints per provider — the user-facing flow:
6
+ # GET /auth/oauth/:provider/start → redirect to provider's authorize URL
7
+ # GET /auth/oauth/:provider/callback → exchange code + sign in
8
+ #
9
+ # State token: cryptographically random, stored in the session, and
10
+ # re-verified on callback. Mismatch ⇒ 403. Without this, an attacker
11
+ # can trick a victim into linking the attacker's OAuth identity to
12
+ # the victim's account.
13
+ class CallbacksController < ApplicationController
14
+ skip_before_action :verify_authenticity_token, only: %i[callback]
15
+
16
+ rescue_from Auth::OAuthProviderUnknown do |e|
17
+ redirect_to Auth.configuration.after_sign_out_url, alert: e.message
18
+ end
19
+
20
+ # GET /auth/oauth/:provider/start
21
+ def start
22
+ provider = params.require(:provider)
23
+ Auth.oauth(provider) # raises OAuthProviderUnknown if not configured
24
+
25
+ state = SecureRandom.urlsafe_base64(32)
26
+ session[oauth_state_key(provider)] = state
27
+
28
+ redirect_to Auth.oauth(provider).authorize_url(
29
+ state: state,
30
+ redirect_uri: callback_url(provider)
31
+ ), allow_other_host: true, status: :see_other
32
+ end
33
+
34
+ # GET /auth/oauth/:provider/callback?code=...&state=...
35
+ def callback
36
+ provider = params.require(:provider)
37
+ returned_state = params[:state]
38
+ expected_state = session.delete(oauth_state_key(provider))
39
+
40
+ if expected_state.nil? || returned_state != expected_state
41
+ return redirect_to Auth.configuration.after_sign_out_url, alert: "OAuth state mismatch"
42
+ end
43
+
44
+ if (error = params[:error])
45
+ return redirect_to Auth.configuration.after_sign_out_url,
46
+ alert: "OAuth #{provider}: #{error}"
47
+ end
48
+
49
+ result = Auth::OAuth::Authenticator.call(
50
+ provider: provider,
51
+ code: params.require(:code),
52
+ redirect_uri: callback_url(provider)
53
+ )
54
+
55
+ if result.ok?
56
+ cookies.encrypted[Auth.configuration.cookie_name] = {
57
+ value: result.session.token,
58
+ httponly: true,
59
+ secure: request.ssl?,
60
+ expires: result.session.expires_at
61
+ }
62
+ redirect_to Auth.configuration.after_sign_in_url,
63
+ notice: result.new_identity ? "Welcome!" : "Signed in"
64
+ else
65
+ redirect_to Auth.configuration.after_sign_out_url, alert: result.error
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def oauth_state_key(provider)
72
+ "auth_oauth_state_#{provider}"
73
+ end
74
+
75
+ def callback_url(provider)
76
+ url_for(action: :callback, provider: provider, only_path: false)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class PasswordResetsController < ApplicationController
5
+ # 5 reset requests per hour per IP. Cheap to send mail but
6
+ # creating reset tokens for every email is a guess-the-user attack
7
+ # vector — rate-limit modestly.
8
+ rate_limit to: 5, within: 1.hour, only: %i[create update],
9
+ with: -> { redirect_to auth.new_session_path, alert: "Too many password-reset requests. Try again later." }
10
+
11
+ # GET /auth/password_reset/new
12
+ def new
13
+ end
14
+
15
+ # POST /auth/password_reset
16
+ def create
17
+ Auth::ResetPassword.request(email: params[:email])
18
+ # Don't leak whether the email exists — same response either way.
19
+ flash[:notice] = "If that email is registered, a reset link is on its way."
20
+ redirect_to new_session_path
21
+ end
22
+
23
+ # GET /auth/password_reset/edit?token=...
24
+ def edit
25
+ @token = params[:token]
26
+ end
27
+
28
+ # PATCH /auth/password_reset
29
+ def update
30
+ result = Auth::ResetPassword.complete(
31
+ token: params[:token],
32
+ new_password: params[:password]
33
+ )
34
+
35
+ if result.ok?
36
+ redirect_to new_session_path, notice: "Password updated. Sign in with your new password."
37
+ else
38
+ flash.now[:alert] = result.error
39
+ @token = params[:token]
40
+ render :edit, status: :unprocessable_entity
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class RegistrationsController < ApplicationController
5
+ # 5 sign-ups per hour per IP. Tighter than sign-in because each
6
+ # successful row also sends a welcome email + provisions side
7
+ # effects. Rails 8's built-in rate_limit uses Solid Cache.
8
+ rate_limit to: 5, within: 1.hour, only: %i[create],
9
+ with: -> { redirect_to auth.new_registration_path, alert: "Too many sign-ups from this IP. Try again later." }
10
+
11
+ def new
12
+ @identity = Auth::Identity.new
13
+ end
14
+
15
+ def create
16
+ result = Auth::RegisterIdentity.call(
17
+ email: params.dig(:identity, :email),
18
+ password: params.dig(:identity, :password),
19
+ password_confirmation: params.dig(:identity, :password_confirmation)
20
+ )
21
+
22
+ if result.ok?
23
+ cookies.encrypted[Auth.configuration.cookie_name] = {
24
+ value: result.session.token, httponly: true, expires: result.session.expires_at
25
+ }
26
+ redirect_to Auth.configuration.after_sign_in_url, notice: "Welcome"
27
+ else
28
+ @identity = Auth::Identity.new(email: params.dig(:identity, :email))
29
+ flash.now[:alert] = result.error
30
+ render :new, status: :unprocessable_entity
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class SessionsController < ApplicationController
5
+ # 10 attempts per minute per IP. Rails 8's built-in rate_limit
6
+ # uses Solid Cache by default; configure cache_store in your host
7
+ # if you need to override.
8
+ rate_limit to: 10, within: 1.minute, only: %i[create],
9
+ with: -> { redirect_to auth.new_session_path, alert: "Too many attempts. Try again in a minute." }
10
+
11
+ def new
12
+ # render the sign-in form
13
+ end
14
+
15
+ def create
16
+ result = Auth::AuthenticateIdentity.call(
17
+ email: params[:email], password: params[:password]
18
+ )
19
+
20
+ if result.ok?
21
+ cookies.encrypted[Auth.configuration.cookie_name] = {
22
+ value: result.session.token, httponly: true, expires: result.session.expires_at
23
+ }
24
+ redirect_to Auth.configuration.after_sign_in_url, notice: "Signed in"
25
+ else
26
+ flash.now[:alert] = result.error
27
+ render :new, status: :unprocessable_entity
28
+ end
29
+ end
30
+
31
+ def destroy
32
+ token = cookies.encrypted[Auth.configuration.cookie_name]
33
+ if token
34
+ session = Auth::Session.find_by(token: token)
35
+ if session
36
+ identity = session.identity
37
+ Seams::Events::Publisher.publish(
38
+ "identity.signed_out.auth",
39
+ identity_id: identity.id,
40
+ session_id: session.id
41
+ )
42
+ session.destroy
43
+ end
44
+ end
45
+ cookies.delete(Auth.configuration.cookie_name)
46
+ redirect_to Auth.configuration.after_sign_out_url, notice: "Signed out"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class ApplicationJob < ::ApplicationJob
5
+ queue_as :default
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Sweeps expired Auth::Session rows. Wire as a Solid Queue Recurring
5
+ # entry in config/recurring.yml:
6
+ #
7
+ # auth_cleanup_expired_sessions:
8
+ # class: Auth::CleanupExpiredSessionsJob
9
+ # schedule: "every 1 hour"
10
+ #
11
+ # Publishes session.expired.auth for each row destroyed so any
12
+ # downstream listener (Notifications, audit log) can react.
13
+ class CleanupExpiredSessionsJob < ApplicationJob
14
+ queue_as :auth
15
+
16
+ BATCH_SIZE = 500
17
+
18
+ def perform
19
+ Auth::Session.where(expires_at: ..Time.current).find_each(batch_size: BATCH_SIZE) do |session|
20
+ identity = session.identity
21
+ Seams::Events::Publisher.publish(
22
+ "session.expired.auth",
23
+ identity_id: identity&.id,
24
+ session_id: session.id
25
+ )
26
+ session.destroy
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class PasswordsMailer < ::ApplicationMailer
5
+ def reset_email(identity)
6
+ @identity = identity
7
+ # Rails 8 has_secure_password defines `password_reset_token` —
8
+ # a signed_id with a 15-minute default expiry. The mailer is
9
+ # responsible for calling it (so the token is generated as
10
+ # late as possible to maximise validity).
11
+ @token = identity.password_reset_token
12
+ mail(to: identity.email, subject: "Reset your password")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "securerandom"
5
+
6
+ module Auth
7
+ # Bearer-style API token. Issued to an Identity, presented in the
8
+ # Authorization header by API clients.
9
+ #
10
+ # Storage: only a SHA-256 hash of the token lives in the DB —
11
+ # `token_digest`. The plaintext (`token`) is shown ONCE, at
12
+ # creation time, via the GenerateApiToken service. If the identity
13
+ # loses it they must rotate; we cannot recover it.
14
+ #
15
+ # `token_prefix` is the first few characters of the plaintext, kept
16
+ # in the DB for identification in dashboards / audit logs without
17
+ # exposing the secret. Format: `seam_<8 random chars>` so it's
18
+ # easy to grep for.
19
+ #
20
+ # Optional `expires_at`. Optional `last_used_at` for activity
21
+ # tracking. `name` lets the identity label the token ("CI deploy key",
22
+ # "iPhone shortcut").
23
+ class ApiToken < ApplicationRecord
24
+ self.table_name = "auth_api_tokens"
25
+
26
+ PREFIX = "seam_"
27
+ PLAINTEXT_LENGTH = 32 # bytes → 43 chars urlsafe-base64
28
+ PREFIX_DISPLAY = 12 # chars stored in token_prefix
29
+
30
+ belongs_to :identity, class_name: "Auth::Identity", foreign_key: :identity_id
31
+
32
+ validates :token_digest, presence: true, uniqueness: true
33
+ validates :token_prefix, presence: true
34
+ validates :name, presence: true
35
+
36
+ scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
37
+ scope :expired, -> { where(expires_at: ..Time.current) }
38
+
39
+ def expired?
40
+ expires_at.present? && expires_at <= Time.current
41
+ end
42
+
43
+ def touch_last_used!
44
+ update_column(:last_used_at, Time.current) # rubocop:disable Rails/SkipsModelValidations
45
+ end
46
+
47
+ # Hash a plaintext token the same way #find_by_plaintext does, so
48
+ # callers can verify a candidate token without exposing the digest.
49
+ def self.digest(plaintext)
50
+ Digest::SHA256.hexdigest(plaintext.to_s)
51
+ end
52
+
53
+ # Returns the ApiToken matching the plaintext, or nil. Does NOT
54
+ # touch last_used_at — callers do that explicitly so a passive
55
+ # presence check doesn't bump the timestamp.
56
+ def self.find_by_plaintext(plaintext)
57
+ return nil if plaintext.to_s.empty?
58
+
59
+ find_by(token_digest: digest(plaintext))
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # ActiveSupport::CurrentAttributes namespace for the Auth engine.
5
+ # Set once per request by the Authentication concern; readable from
6
+ # anywhere downstream (services, models, jobs that re-resolve via
7
+ # the same controller stack) without explicit threading.
8
+ #
9
+ # Wave 9 Phase 1a only sets `identity`. Phase 1b will add
10
+ # `Current.account`; Phase 2 will add `Current.user` (the account
11
+ # membership, NOT a host User).
12
+ class Current < ActiveSupport::CurrentAttributes
13
+ attribute :identity
14
+ end
15
+ end