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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # The canonical "who is logging in" record. Owns credentials
5
+ # (password digest, OAuth links, API tokens, sessions). Lives on
6
+ # `auth_identities` — a dedicated table so credentials are cleanly
7
+ # separated from any host-specific User concept (a "human" can exist
8
+ # in many hosts as the same Identity if they share a database, but
9
+ # have different per-host User profiles).
10
+ #
11
+ # Other engines address the human via Identity, not via a host User.
12
+ class Identity < ApplicationRecord
13
+ self.table_name = "auth_identities"
14
+
15
+ # Rails 8's has_secure_password ships the password-reset token
16
+ # machinery: `password_reset_token` (instance method, returns a
17
+ # signed_id with a 15-minute default expiry) and
18
+ # `find_by_password_reset_token(token, purpose: :password_reset)`.
19
+ # Once `Identity` has its own table the historical naming clash
20
+ # with a `password_reset_token` *column* is gone.
21
+ has_secure_password
22
+ has_many :sessions, class_name: "Auth::Session", foreign_key: :identity_id, dependent: :destroy
23
+ has_many :api_tokens, class_name: "Auth::ApiToken", foreign_key: :identity_id, dependent: :destroy
24
+ has_many :oauth_providers, class_name: "Auth::OAuth::Provider", foreign_key: :identity_id, dependent: :destroy
25
+
26
+ # Email is PII (GDPR Article 4). Stored encrypted at rest via Rails 7+
27
+ # ActiveRecord::Encryption. `deterministic: true` keeps `find_by(email:)`
28
+ # and the uniqueness index working — same plaintext yields the same
29
+ # ciphertext. `downcase: true` normalises before encryption so two
30
+ # casings of the same address collide as expected.
31
+ # Host setup: bin/rails db:encryption:init (one-off).
32
+ # See https://guides.rubyonrails.org/active_record_encryption.html
33
+ encrypts :email, deterministic: true, downcase: true
34
+
35
+ validates :email, presence: true, uniqueness: { case_sensitive: false }
36
+ validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
37
+ validates :password,
38
+ length: { minimum: -> { Auth.configuration.password_min_length } },
39
+ if: -> { password.present? }
40
+
41
+ normalizes :email, with: ->(value) { value.to_s.strip.downcase }
42
+
43
+ # Throwaway bcrypt hash to soak up the cost-12 ~100ms when the
44
+ # email lookup misses. Without this, `Identity.authenticate(email: ...)`
45
+ # returns in ~5ms for an unknown email and ~100ms for a known one
46
+ # — a measurable timing oracle that lets attackers enumerate
47
+ # registered identities. We pre-compute one digest at class load
48
+ # so every miss runs the same bcrypt work.
49
+ DUMMY_PASSWORD_DIGEST = BCrypt::Password.create("never-matches-anything", cost: BCrypt::Engine.cost).freeze
50
+
51
+ # Returns the Identity on success, nil on failure (no such identity OR
52
+ # wrong password). bcrypt's `#authenticate` returns `false` on
53
+ # failure; we coerce to `nil` so callers can use a uniform
54
+ # `if identity = Identity.authenticate(...)` idiom.
55
+ def self.authenticate(email:, password:)
56
+ identity = find_by(email: email.to_s.strip.downcase)
57
+ if identity
58
+ identity.authenticate(password) ? identity : nil
59
+ else
60
+ # Constant-time defence against enumeration: do the same
61
+ # bcrypt work even on a miss, then return nil.
62
+ BCrypt::Password.new(DUMMY_PASSWORD_DIGEST).is_password?(password.to_s)
63
+ nil
64
+ end
65
+ end
66
+
67
+ # Platform-admin predicate. Read-only convenience over the staff
68
+ # column; host admin tooling can call `identity.staff?` to bypass
69
+ # account-scope checks for support flows.
70
+ def staff?
71
+ !!staff
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module OAuth
5
+ # Links an Auth::Identity to an external OAuth identity (Google
6
+ # account, GitHub account, etc.). One row per (identity, provider)
7
+ # pair — multiple rows per identity means the identity signed in
8
+ # with multiple providers, and the identity can be looked up via
9
+ # either.
10
+ #
11
+ # Tokens are encrypted at the database level via Rails 7+
12
+ # ActiveRecord::Encryption. Run `bin/rails db:encryption:init` once
13
+ # in the host to set the keys, then store them as Rails credentials
14
+ # — see https://guides.rubyonrails.org/active_record_encryption.html.
15
+ class Provider < ApplicationRecord
16
+ self.table_name = "auth_oauth_providers"
17
+
18
+ PROVIDERS = %w[google github].freeze
19
+
20
+ belongs_to :identity, class_name: "Auth::Identity", foreign_key: :identity_id
21
+
22
+ validates :provider, presence: true, inclusion: { in: PROVIDERS }
23
+ validates :provider_uid, presence: true,
24
+ uniqueness: { scope: :provider,
25
+ message: "is already linked to another account" }
26
+ validates :identity_id, presence: true,
27
+ uniqueness: { scope: :provider,
28
+ message: "already linked this provider" }
29
+
30
+ # access_token / refresh_token are credentials — encrypted with
31
+ # the default (non-deterministic) mode for maximum strength. We
32
+ # never query by these.
33
+ encrypts :access_token
34
+ encrypts :refresh_token
35
+
36
+ # provider_uid is the identity's stable id at the OAuth provider
37
+ # (Google `sub`, GitHub user id). It IS personal data under GDPR
38
+ # Article 4 ("online identifier"). Deterministic so the
39
+ # (provider, provider_uid) lookup that powers OAuth sign-in keeps
40
+ # resolving and the unique index keeps enforcing.
41
+ encrypts :provider_uid, deterministic: true
42
+
43
+ def access_token_expired?
44
+ expires_at && expires_at < Time.current
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class Session < ApplicationRecord
5
+ self.table_name = "auth_sessions"
6
+
7
+ belongs_to :identity, class_name: "Auth::Identity"
8
+
9
+ before_create :assign_token
10
+ before_create :assign_expiry
11
+
12
+ scope :active, -> { where("expires_at > ?", Time.current) }
13
+
14
+ def expired?
15
+ expires_at <= Time.current
16
+ end
17
+
18
+ private
19
+
20
+ def assign_token
21
+ self.token ||= SecureRandom.hex(32)
22
+ end
23
+
24
+ def assign_expiry
25
+ self.expires_at ||= Time.current + Auth.configuration.session_ttl
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Sign-in service. Validates credentials, creates a session, returns
5
+ # a Result struct: ok?, identity, session, error.
6
+ class AuthenticateIdentity
7
+ Result = Struct.new(:ok?, :identity, :session, :error, keyword_init: true)
8
+
9
+ def self.call(...)
10
+ new(...).call
11
+ end
12
+
13
+ def initialize(email:, password:)
14
+ @email = email
15
+ @password = password
16
+ end
17
+
18
+ def call
19
+ identity = Auth::Identity.authenticate(email: @email, password: @password)
20
+ return Result.new(ok?: false, error: "Invalid email or password") unless identity
21
+
22
+ session = identity.sessions.create!
23
+ Seams::Events::Publisher.publish(
24
+ "identity.signed_in.auth",
25
+ identity_id: identity.id,
26
+ session_id: session.id
27
+ )
28
+ Result.new(ok?: true, identity: identity, session: session)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Auth
6
+ # Issues a new API token for an identity. Returns a Result with both
7
+ # the persisted ApiToken row AND the plaintext (which is the only
8
+ # time it's available — the DB only stores the digest).
9
+ module GenerateApiToken
10
+ Result = Struct.new(:ok?, :api_token, :plaintext, :error, keyword_init: true)
11
+
12
+ module_function
13
+
14
+ def call(identity:, name:, expires_at: nil)
15
+ plaintext = "#{ApiToken::PREFIX}#{SecureRandom.urlsafe_base64(ApiToken::PLAINTEXT_LENGTH)}"
16
+ record = identity.api_tokens.create!(
17
+ name: name,
18
+ token_digest: ApiToken.digest(plaintext),
19
+ token_prefix: plaintext[0, ApiToken::PREFIX_DISPLAY],
20
+ expires_at: expires_at
21
+ )
22
+
23
+ Seams::Events::Publisher.publish(
24
+ "api_token.issued.auth",
25
+ identity_id: identity.id,
26
+ api_token_id: record.id,
27
+ token_prefix: record.token_prefix
28
+ )
29
+
30
+ Result.new(ok?: true, api_token: record, plaintext: plaintext)
31
+ rescue ActiveRecord::RecordInvalid => e
32
+ Result.new(ok?: false, error: e.message)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module OAuth
5
+ # Orchestrates the OAuth callback flow:
6
+ #
7
+ # 1. Exchange the authorization `code` for an access token.
8
+ # 2. Fetch the provider's user profile.
9
+ # 3. Find an existing Auth::OAuth::Provider row by (provider,
10
+ # provider_uid) OR find an existing Auth::Identity by email OR
11
+ # create a new Auth::Identity. Link the row to the identity.
12
+ # 4. Refresh stored token + profile.
13
+ # 5. Create a new Auth::Session.
14
+ # 6. Publish identity.signed_up.auth (first sign-in via OAuth) or
15
+ # identity.signed_in.auth (returning identity).
16
+ #
17
+ # Returns a Result: ok?, identity, session, oauth_provider, new_identity, error.
18
+ class Authenticator
19
+ Result = Struct.new(:ok?, :identity, :session, :oauth_provider, :new_identity, :error,
20
+ keyword_init: true)
21
+
22
+ def self.call(...)
23
+ new(...).call
24
+ end
25
+
26
+ def initialize(provider:, code:, redirect_uri:)
27
+ @provider = provider.to_s
28
+ @code = code
29
+ @redirect_uri = redirect_uri
30
+ end
31
+
32
+ def call
33
+ adapter = Auth.oauth(@provider)
34
+ tokens = adapter.exchange_code(code: @code, redirect_uri: @redirect_uri)
35
+ profile = adapter.fetch_user_info(access_token: tokens[:access_token])
36
+
37
+ return Result.new(ok?: false, error: "OAuth provider returned no email") if profile.email.blank?
38
+
39
+ oauth_row, identity, new_identity = link_or_create(profile, tokens)
40
+ session = identity.sessions.create!
41
+
42
+ Seams::Events::Publisher.publish(
43
+ new_identity ? "identity.signed_up.auth" : "identity.signed_in.auth",
44
+ identity_id: identity.id,
45
+ session_id: session.id,
46
+ email: identity.email
47
+ )
48
+
49
+ Result.new(ok?: true, identity: identity, session: session,
50
+ oauth_provider: oauth_row, new_identity: new_identity)
51
+ rescue Auth::OAuthError, ActiveRecord::RecordInvalid => e
52
+ Result.new(ok?: false, error: e.message)
53
+ end
54
+
55
+ private
56
+
57
+ def link_or_create(profile, tokens)
58
+ Provider.transaction do
59
+ oauth_row = Provider.find_by(provider: @provider, provider_uid: profile.provider_uid)
60
+
61
+ new_identity = false
62
+ identity =
63
+ if oauth_row
64
+ oauth_row.identity
65
+ else
66
+ existing = Auth::Identity.find_by(email: profile.email.to_s.downcase)
67
+ existing || begin
68
+ new_identity = true
69
+ # Random unguessable password — OAuth identities don't have a
70
+ # password but the column is required. They sign in via
71
+ # the provider; password reset lets them set one later.
72
+ Auth::Identity.create!(
73
+ email: profile.email,
74
+ password: SecureRandom.urlsafe_base64(32)
75
+ )
76
+ end
77
+ end
78
+
79
+ oauth_row ||= Provider.new(provider: @provider, provider_uid: profile.provider_uid, identity: identity)
80
+ oauth_row.assign_attributes(
81
+ access_token: tokens[:access_token],
82
+ refresh_token: tokens[:refresh_token],
83
+ expires_at: tokens[:expires_in] ? Time.current + tokens[:expires_in].to_i : nil,
84
+ token_type: tokens[:token_type] || "Bearer",
85
+ profile_data: profile.raw
86
+ )
87
+ oauth_row.save!
88
+
89
+ [oauth_row, identity, new_identity]
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Sign-up service. Creates the identity + first session in a transaction
5
+ # and publishes identity.signed_up.auth on success.
6
+ #
7
+ # Returns a Result struct: ok?, identity, session, error.
8
+ class RegisterIdentity
9
+ Result = Struct.new(:ok?, :identity, :session, :error, keyword_init: true)
10
+
11
+ # Whitelist of extra attributes a caller can hand in via
12
+ # `attributes:`. Anything outside this list is dropped — closes the
13
+ # mass-assignment hole where internal columns could be set from
14
+ # forwarded params. Add to the list as the auth_identities schema
15
+ # grows. NB: `staff` is intentionally NOT here — promotion is an
16
+ # admin operation, not a sign-up surface.
17
+ PERMITTED_EXTRA_ATTRIBUTES = %i[].freeze
18
+
19
+ def self.call(...)
20
+ new(...).call
21
+ end
22
+
23
+ def initialize(email:, password:, password_confirmation: nil, attributes: {})
24
+ @email = email
25
+ @password = password
26
+ @password_confirmation = password_confirmation
27
+ @attributes = sanitize(attributes)
28
+ end
29
+
30
+ def call
31
+ identity, session = nil
32
+ Auth::Identity.transaction do
33
+ identity = Auth::Identity.create!(@attributes.merge(
34
+ email: @email, password: @password, password_confirmation: @password_confirmation
35
+ ))
36
+ session = identity.sessions.create!
37
+ end
38
+
39
+ Seams::Events::Publisher.publish(
40
+ "identity.signed_up.auth",
41
+ identity_id: identity.id,
42
+ email: identity.email
43
+ )
44
+ Result.new(ok?: true, identity: identity, session: session)
45
+ rescue ActiveRecord::RecordInvalid => e
46
+ Result.new(ok?: false, error: e.message)
47
+ end
48
+
49
+ private
50
+
51
+ def sanitize(attrs)
52
+ return {} if attrs.nil? || attrs.empty?
53
+
54
+ attrs.to_h.transform_keys(&:to_sym).slice(*PERMITTED_EXTRA_ATTRIBUTES)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Two-phase password reset, backed by Rails 8's built-in
5
+ # `has_secure_password` reset_token feature. The token is a signed_id
6
+ # with a built-in 15-minute expiry — no column, no sweep job, no
7
+ # `password_reset_token_sent_at` needed.
8
+ #
9
+ # Auth::ResetPassword.request(email: "x@y.com")
10
+ # → looks up the identity, generates a signed_id token, emails it
11
+ #
12
+ # Auth::ResetPassword.complete(token: "...", new_password: "...")
13
+ # → finds the identity by signed_id (Rails verifies expiry +
14
+ # purpose), updates the password
15
+ #
16
+ # Both phases return a Result struct so the controller has a uniform
17
+ # success/failure shape regardless of which phase failed.
18
+ module ResetPassword
19
+ Result = Struct.new(:ok?, :identity, :error, keyword_init: true)
20
+
21
+ module_function
22
+
23
+ def request(email:)
24
+ identity = Auth::Identity.find_by(email: email.to_s.strip.downcase)
25
+ return Result.new(ok?: true) unless identity # don't leak which emails are registered
26
+
27
+ Auth::PasswordsMailer.reset_email(identity).deliver_later
28
+ Result.new(ok?: true, identity: identity)
29
+ end
30
+
31
+ def complete(token:, new_password:)
32
+ identity = Auth::Identity.find_by_password_reset_token(token)
33
+ return Result.new(ok?: false, error: "Invalid or expired reset link") unless identity
34
+
35
+ identity.update!(password: new_password)
36
+ Result.new(ok?: true, identity: identity)
37
+ rescue ActiveRecord::RecordInvalid => e
38
+ Result.new(ok?: false, error: e.message)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Destroys an Auth::ApiToken row and publishes the canonical
5
+ # api_token.revoked.auth event so subscribers (notifications,
6
+ # audit log) can react. Returns a Result with the same shape as
7
+ # GenerateApiToken so callers can branch uniformly.
8
+ #
9
+ # result = Auth::RevokeApiToken.call(api_token: token)
10
+ # result.ok? # => true
11
+ # result.api_token.destroyed? # => true
12
+ #
13
+ # Idempotent: revoking an already-destroyed token returns an
14
+ # ok? = false Result with code: :not_found rather than raising.
15
+ module RevokeApiToken
16
+ Result = Struct.new(:ok?, :api_token, :error, :code, keyword_init: true)
17
+
18
+ module_function
19
+
20
+ def call(api_token:)
21
+ return Result.new(ok?: false, error: "API token not found", code: :not_found) if api_token.nil? || api_token.destroyed?
22
+
23
+ identity = api_token.identity
24
+ api_token_id = api_token.id
25
+ token_prefix = api_token.token_prefix
26
+ api_token.destroy!
27
+
28
+ Seams::Events::Publisher.publish(
29
+ "api_token.revoked.auth",
30
+ identity_id: identity&.id,
31
+ api_token_id: api_token_id,
32
+ token_prefix: token_prefix
33
+ )
34
+
35
+ Result.new(ok?: true, api_token: api_token)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ <h1>Choose a new password</h1>
2
+
3
+ <%% if flash[:alert] %><p style="color: red"><%%= flash[:alert] %></p><%% end %>
4
+
5
+ <%%= form_with url: password_reset_path, method: :patch, local: true do |f| %>
6
+ <%%= hidden_field_tag :token, @token %>
7
+ <p>
8
+ <%%= label_tag :password, "New password" %>
9
+ <%%= password_field_tag :password, nil, required: true, autofocus: true %>
10
+ </p>
11
+ <%%= f.submit "Update password" %>
12
+ <%% end %>
@@ -0,0 +1,11 @@
1
+ <h1>Forgot your password?</h1>
2
+
3
+ <%%= form_with url: password_reset_path, method: :post, local: true do |f| %>
4
+ <p>
5
+ <%%= label_tag :email %>
6
+ <%%= email_field_tag :email, nil, required: true, autofocus: true %>
7
+ </p>
8
+ <%%= f.submit "Send reset link" %>
9
+ <%% end %>
10
+
11
+ <p><%%= link_to "Back to sign in", new_session_path %></p>
@@ -0,0 +1,7 @@
1
+ <h1>Reset your password</h1>
2
+
3
+ <p>Click the link below to set a new password. The link is valid for 15 minutes.</p>
4
+
5
+ <p><%%= link_to "Reset password", edit_password_reset_url(token: @token) %></p>
6
+
7
+ <p>If you didn't request this, ignore this email — your password won't change.</p>
@@ -0,0 +1,26 @@
1
+ <h1>Create an account</h1>
2
+
3
+ <%%= form_with model: @identity, url: registration_path, scope: :identity, local: true do |f| %>
4
+ <%% if @identity.errors.any? %>
5
+ <ul>
6
+ <%% @identity.errors.full_messages.each do |msg| %>
7
+ <li><%%= msg %></li>
8
+ <%% end %>
9
+ </ul>
10
+ <%% end %>
11
+ <p>
12
+ <%%= f.label :email %>
13
+ <%%= f.email_field :email, required: true, autofocus: true %>
14
+ </p>
15
+ <p>
16
+ <%%= f.label :password %>
17
+ <%%= f.password_field :password, required: true %>
18
+ </p>
19
+ <p>
20
+ <%%= f.label :password_confirmation %>
21
+ <%%= f.password_field :password_confirmation, required: true %>
22
+ </p>
23
+ <%%= f.submit "Sign up" %>
24
+ <%% end %>
25
+
26
+ <p><%%= link_to "Already have an account? Sign in", new_session_path %></p>
@@ -0,0 +1,18 @@
1
+ <%%# Renders one "Sign in with X" link per configured OAuth provider. %>
2
+ <%%# Drop into the sign-in / sign-up form: %>
3
+ <%%# <%%= render "auth/sessions/oauth_buttons" %> %>
4
+ <%%# Override by replacing the file in your host. %>
5
+ <%% if Auth.configuration.oauth_providers.any? %>
6
+ <section class="auth-oauth-buttons">
7
+ <p>Or sign in with:</p>
8
+ <ul>
9
+ <%% Auth.configuration.oauth_providers.each_key do |provider| %>
10
+ <li>
11
+ <%%= link_to "Sign in with #{provider.to_s.titleize}",
12
+ auth.oauth_start_path(provider: provider),
13
+ class: "auth-oauth-button auth-oauth-#{provider}" %>
14
+ </li>
15
+ <%% end %>
16
+ </ul>
17
+ </section>
18
+ <%% end %>
@@ -0,0 +1,17 @@
1
+ <h1>Sign in</h1>
2
+
3
+ <%% if flash[:alert] %><p style="color: red"><%%= flash[:alert] %></p><%% end %>
4
+
5
+ <%%= form_with url: session_path, method: :post, local: true do |f| %>
6
+ <p>
7
+ <%%= label_tag :email %>
8
+ <%%= email_field_tag :email, nil, required: true, autofocus: true %>
9
+ </p>
10
+ <p>
11
+ <%%= label_tag :password %>
12
+ <%%= password_field_tag :password, nil, required: true %>
13
+ </p>
14
+ <%%= f.submit "Sign in" %>
15
+ <%% end %>
16
+
17
+ <p><%%= link_to "Create an account", new_registration_path %></p>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ Auth::Engine.routes.draw do
4
+ # Follow-up generators that ship sign-in alternatives (passkeys, magic links, SSO) splice their routes here, ahead of the password session resource so they take precedence.
5
+ # seams:insertion-point auth.routes.before_session
6
+ resource :session, only: %i[new create destroy], controller: :sessions
7
+ resource :registration, only: %i[new create], controller: :registrations
8
+ resource :password_reset, only: %i[new create edit update]
9
+
10
+ # OAuth — one URL pair per configured provider. The :provider param
11
+ # is matched against Auth.configuration.oauth_providers at request
12
+ # time; unknown providers raise Auth::OAuthProviderUnknown which
13
+ # the controller handles.
14
+ scope "/oauth/:provider" do
15
+ get "start", to: "oauth/callbacks#start", as: :oauth_start
16
+ get "callback", to: "oauth/callbacks#callback", as: :oauth_callback
17
+ end
18
+
19
+ # Follow-up generators that add NEW route surfaces (API token UI, social-link admin) splice their resources here.
20
+ # seams:insertion-point auth.routes.after_oauth
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates auth_api_tokens — Bearer-style API tokens issued to
4
+ # Auth::Identity rows for programmatic access.
5
+ # Why: the engine ships native API token support so hosts don't
6
+ # roll their own. Plaintext is shown once at creation; only the
7
+ # SHA-256 digest is persisted.
8
+ # Risk: append-mostly. Unique index on token_digest enforces no
9
+ # collisions (cosmetic — SHA-256 collision space is huge).
10
+ # Partial index on expires_at speeds up the .active scope.
11
+ class CreateAuthApiTokens < ActiveRecord::Migration[7.1]
12
+ def change
13
+ create_table :auth_api_tokens do |t|
14
+ t.references :identity, null: false, foreign_key: { to_table: :auth_identities }
15
+ t.string :name, null: false # human label ("CI deploy key")
16
+ t.string :token_digest, null: false # SHA-256 of the plaintext
17
+ t.string :token_prefix, null: false # first ~12 chars for display
18
+ t.datetime :expires_at # nil = never expires
19
+ t.datetime :last_used_at # nil = never used
20
+ t.timestamps
21
+ end
22
+
23
+ add_index :auth_api_tokens, :token_digest, unique: true
24
+ add_index :auth_api_tokens, :expires_at, where: "expires_at IS NOT NULL"
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the auth_identities table for the Auth engine.
4
+ # Why: Auth::Identity owns credentials (password digest, email) and
5
+ # acts as the canonical "who is logging in" record. Other engines
6
+ # address the human via Identity, not via the host's own User.
7
+ # Risk: empty table on creation — no data migration needed.
8
+ class CreateAuthIdentities < ActiveRecord::Migration[7.1]
9
+ def change
10
+ create_table :auth_identities do |t|
11
+ # `:text` (not `:string`) because `email` is encrypted via
12
+ # ActiveRecord::Encryption (deterministic). Stripe-style envelope
13
+ # ciphertext + base64 + IV/key-id headers expand a 30-char email
14
+ # to ~150–250 bytes; on MySQL `:string` defaults to VARCHAR(255)
15
+ # which silently truncates the cipher and breaks decryption.
16
+ # https://guides.rubyonrails.org/active_record_encryption.html#about-storage-and-column-size
17
+ t.text :email, null: false
18
+ t.string :password_digest, null: false
19
+ # Platform-admin flag. Identity-level (not Account-scoped) so
20
+ # host admin tooling can bypass account scoping for support.
21
+ # Default false — explicit opt-in only.
22
+ t.boolean :staff, null: false, default: false
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :auth_identities, :email, unique: true
27
+ add_index :auth_identities, :staff, where: "staff = true"
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates auth_oauth_providers — links Auth::Identity to external
4
+ # OAuth identities (Google, GitHub, etc.). One row per (identity,
5
+ # provider) pair.
6
+ # Why: the engine ships native OAuth support so hosts don't roll their
7
+ # own. Tokens are encrypted at the column level via
8
+ # ActiveRecord::Encryption — the host must run
9
+ # `bin/rails db:encryption:init` to seed the keys.
10
+ # Risk: append-mostly. profile_data is a json column for the raw
11
+ # provider response (sub/login/etc) so we can re-derive things
12
+ # like display name without re-fetching.
13
+ class CreateAuthOauthProviders < ActiveRecord::Migration[7.1]
14
+ def change
15
+ create_table :auth_oauth_providers do |t|
16
+ t.references :identity, null: false, foreign_key: { to_table: :auth_identities }
17
+ t.string :provider, null: false
18
+ # `:text` because `provider_uid` is encrypted via
19
+ # ActiveRecord::Encryption (deterministic) — see Wave 11. Same
20
+ # ciphertext-overflow concern as auth_identities.email.
21
+ t.text :provider_uid, null: false
22
+ # encrypts :access_token / :refresh_token store ciphertext as
23
+ # text. ActiveRecord::Encryption handles the (de)cipher.
24
+ t.text :access_token
25
+ t.text :refresh_token
26
+ t.datetime :expires_at
27
+ t.string :token_type, default: "Bearer"
28
+ t.jsonb :profile_data, null: false, default: {}
29
+ t.timestamps
30
+ end
31
+
32
+ add_index :auth_oauth_providers, %i[provider provider_uid], unique: true
33
+ add_index :auth_oauth_providers, %i[identity_id provider], unique: true
34
+ end
35
+ end