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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # What: creates the auth_sessions table for the Auth engine.
4
+ # Why: Auth::Session backs the encrypted cookie set on sign-in; we
5
+ # store the token + expiry so sign-out can revoke just one device.
6
+ # Risk: empty table on creation — no data migration needed.
7
+ class CreateAuthSessions < ActiveRecord::Migration[7.1]
8
+ def change
9
+ create_table :auth_sessions do |t|
10
+ t.references :identity, null: false, foreign_key: { to_table: :auth_identities }, index: true
11
+ t.string :token, null: false
12
+ t.datetime :expires_at, null: false
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :auth_sessions, :token, unique: true
17
+ add_index :auth_sessions, :expires_at
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "auth/version"
4
+ require "auth/configuration"
5
+ require "auth/engine"
6
+ require "auth/concerns/authenticatable"
7
+ require "auth/concerns/authentication"
8
+
9
+ module Auth
10
+ class Error < StandardError; end
11
+ class OAuthError < Error; end
12
+ class OAuthProviderUnknown < OAuthError; end
13
+
14
+ class << self
15
+ def configuration
16
+ @configuration ||= Configuration.new
17
+ end
18
+
19
+ def configure
20
+ yield configuration
21
+ end
22
+
23
+ # Build a configured OAuth adapter for the given provider key.
24
+ # Reads `Auth.configuration.oauth_providers[name]` for the adapter
25
+ # class + client_id + client_secret + scopes; raises if the
26
+ # provider isn't configured.
27
+ def oauth(provider_name)
28
+ conf = configuration.oauth_providers[provider_name.to_sym]
29
+ raise OAuthProviderUnknown, "OAuth provider #{provider_name.inspect} is not configured" unless conf
30
+
31
+ adapter_class = Object.const_get(conf.fetch(:adapter))
32
+ adapter_class.new(
33
+ client_id: conf.fetch(:client_id),
34
+ client_secret: conf.fetch(:client_secret),
35
+ scopes: conf[:scopes] || adapter_class::DEFAULT_SCOPES
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Auth
6
+ # Bearer-token controller authentication. Mix into API controllers
7
+ # that should accept `Authorization: Bearer <token>` instead of the
8
+ # session cookie.
9
+ #
10
+ # class Api::WidgetsController < ApplicationController
11
+ # include Auth::ApiAuthenticatable
12
+ # before_action :authenticate_api_token!
13
+ # end
14
+ #
15
+ # Sets `current_api_token` (the ApiToken row) and `current_identity`
16
+ # (the Auth::Identity) on success. Renders 401 with a JSON body on
17
+ # failure. last_used_at is bumped on every successful auth.
18
+ module ApiAuthenticatable
19
+ extend ActiveSupport::Concern
20
+
21
+ included do
22
+ attr_reader :current_api_token
23
+ end
24
+
25
+ def authenticate_api_token!
26
+ token = current_api_token_or_render_401
27
+ return unless token
28
+
29
+ @current_api_token = token
30
+ @current_identity = token.identity
31
+ Auth::Current.identity = @current_identity if defined?(Auth::Current)
32
+ token.touch_last_used!
33
+ end
34
+
35
+ def current_identity
36
+ @current_identity
37
+ end
38
+
39
+ private
40
+
41
+ def current_api_token_or_render_401
42
+ header = request.headers["Authorization"].to_s
43
+ return render_unauthorized!("missing Authorization header") unless header.start_with?("Bearer ")
44
+
45
+ plaintext = header.sub(/\ABearer\s+/, "").strip
46
+ token = Auth::ApiToken.find_by_plaintext(plaintext)
47
+ return render_unauthorized!("invalid token") unless token
48
+ return render_unauthorized!("token expired") if token.expired?
49
+
50
+ token
51
+ end
52
+
53
+ def render_unauthorized!(message)
54
+ render json: { error: message }, status: :unauthorized
55
+ nil
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Auth
6
+ # Concern that the host application's user-facing model can include
7
+ # to gain Auth-engine sign-in tracking, password helpers, and
8
+ # session-aware queries. Listed in this engine's ExposedConcerns so
9
+ # cross-engine boundary cops do not flag callers.
10
+ #
11
+ # OPTIONAL after Wave 9. Most hosts won't need it because
12
+ # `Auth::Identity` is now the canonical "human" record — sessions
13
+ # belong to Identity, not to a host User. Hosts that DO keep a
14
+ # separate User model (e.g. for domain-specific profile fields) and
15
+ # want the sugar can include this concern, but the host User must
16
+ # have an `identity_id` column and the host wires the link itself.
17
+ module Authenticatable
18
+ extend ActiveSupport::Concern
19
+
20
+ included do
21
+ belongs_to :auth_identity, class_name: "Auth::Identity", foreign_key: :identity_id, optional: true
22
+ end
23
+
24
+ def signed_in?
25
+ auth_identity&.sessions&.active&.exists?
26
+ end
27
+
28
+ def sign_out_everywhere!
29
+ auth_identity&.sessions&.destroy_all
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Auth
6
+ # Mix into the host's ApplicationController to gain `current_identity`,
7
+ # `signed_in?`, and `authenticate_identity!` helpers backed by the
8
+ # encrypted Auth session cookie.
9
+ #
10
+ # class ApplicationController < ActionController::Base
11
+ # include Auth::Authentication
12
+ # before_action :authenticate_identity!
13
+ # end
14
+ #
15
+ # Side-effect: when a sign-in is resolved, `Current.identity` is set
16
+ # so models / services downstream can access the signed-in identity
17
+ # without threading it through every method. Future waves will add
18
+ # `Current.account` and `Current.user` (account-membership) — for now
19
+ # this concern only sets `Current.identity`.
20
+ module Authentication
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ helper_method :current_identity, :signed_in? if respond_to?(:helper_method)
25
+ before_action :set_current_identity if respond_to?(:before_action)
26
+ end
27
+
28
+ def current_identity
29
+ @current_identity ||= resolve_current_identity
30
+ end
31
+
32
+ def signed_in?
33
+ current_identity.present?
34
+ end
35
+
36
+ def authenticate_identity!
37
+ return if signed_in?
38
+
39
+ respond_to?(:redirect_to) ? redirect_to(auth_engine.new_session_path) : head(:unauthorized)
40
+ end
41
+
42
+ private
43
+
44
+ def resolve_current_identity
45
+ token = cookies.encrypted[Auth.configuration.cookie_name]
46
+ return nil if token.blank?
47
+
48
+ session = Auth::Session.active.find_by(token: token)
49
+ session&.identity
50
+ end
51
+
52
+ def set_current_identity
53
+ Auth::Current.identity = current_identity if defined?(Auth::Current)
54
+ end
55
+
56
+ def auth_engine
57
+ Auth::Engine.routes.url_helpers
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ # Engine-scoped configuration. Override in
5
+ # config/initializers/auth.rb of the host application:
6
+ #
7
+ # Auth.configure do |c|
8
+ # c.session_ttl = 14.days
9
+ # c.cookie_name = :_my_app_session
10
+ # c.after_sign_in_url = "/dashboard"
11
+ # end
12
+ class Configuration
13
+ attr_accessor :session_ttl, :cookie_name, :after_sign_in_url, :after_sign_out_url,
14
+ :password_min_length, :oauth_providers
15
+ # Follow-up generators that add new top-level configuration knobs (passkey_rp_id, magic_link_ttl) declare their attr_accessor here.
16
+ # seams:insertion-point auth.configuration.attributes
17
+
18
+ def initialize
19
+ @session_ttl = 30 * 24 * 60 * 60 # 30 days, in seconds
20
+ @cookie_name = :auth_session
21
+ @after_sign_in_url = "/"
22
+ @after_sign_out_url = "/"
23
+ @password_min_length = 8
24
+ # OAuth providers — each entry maps a provider key to a config:
25
+ # { adapter: "Auth::OAuth::Google", client_id:, client_secret:, scopes?: }
26
+ # Configure in config/initializers/auth.rb:
27
+ # Auth.configure do |c|
28
+ # c.oauth_providers = {
29
+ # google: { adapter: "Auth::OAuth::Google",
30
+ # client_id: ENV.fetch("GOOGLE_OAUTH_CLIENT_ID"),
31
+ # client_secret: ENV.fetch("GOOGLE_OAUTH_CLIENT_SECRET") },
32
+ # github: { adapter: "Auth::OAuth::Github",
33
+ # client_id: ENV.fetch("GITHUB_OAUTH_CLIENT_ID"),
34
+ # client_secret: ENV.fetch("GITHUB_OAUTH_CLIENT_SECRET") }
35
+ # }
36
+ # end
37
+ @oauth_providers = {
38
+ # Follow-up generators that ship pre-wired OAuth providers splice a `linkedin: { adapter: "Auth::OAuth::LinkedIn", ... }` entry here.
39
+ # seams:insertion-point auth.configuration.oauth_providers
40
+ }
41
+ # Follow-up generators that add defaults for new attributes (matching auth.configuration.attributes) splice them here.
42
+ # seams:insertion-point auth.configuration.defaults
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Auth
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ # Zeitwerk's default inflector lower-cases everything between
12
+ # underscores, so an `oauth/` directory maps to `Oauth::` (single-O)
13
+ # instead of the `OAuth::` (camel-OA) namespace we use for the
14
+ # provider model + authenticator + callbacks controller (and the
15
+ # lib/ adapters Google/Github/Abstract). Single-entry inflection so
16
+ # the override only affects directories named exactly "oauth" — a
17
+ # host's own `oauth_provider.rb` (no trailing directory) stays on
18
+ # the default mapping.
19
+ initializer "auth.zeitwerk_inflections", before: :set_autoload_paths do
20
+ Rails.autoloaders.main.inflector.inflect("oauth" => "OAuth")
21
+ end
22
+
23
+ initializer "auth.register_events" do
24
+ Seams::EventRegistry.register("identity.signed_up.auth", emitted_by: "Auth")
25
+ Seams::EventRegistry.register("identity.signed_in.auth", emitted_by: "Auth")
26
+ Seams::EventRegistry.register("identity.signed_out.auth", emitted_by: "Auth")
27
+ Seams::EventRegistry.register("session.expired.auth", emitted_by: "Auth")
28
+ # API token lifecycle (issue #2 section 2A)
29
+ Seams::EventRegistry.register("api_token.issued.auth", emitted_by: "Auth")
30
+ Seams::EventRegistry.register("api_token.revoked.auth", emitted_by: "Auth")
31
+ # Follow-up generators that emit new auth events (e.g. identity.passkey_added.auth) register them here.
32
+ # seams:insertion-point auth.engine.events
33
+ end
34
+
35
+ initializer "auth.append_migrations" do |app|
36
+ unless app.root == root
37
+ config.paths["db/migrate"].expanded.each do |expanded_path|
38
+ app.config.paths["db/migrate"] << expanded_path
39
+ end
40
+ end
41
+ end
42
+
43
+ # Follow-up generators that need their own initializer block (e.g. attaching a subscriber on an auth event) declare it here.
44
+ # seams:insertion-point auth.engine.initializers
45
+ end
46
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Auth
4
+ module OAuth
5
+ # Contract every OAuth provider adapter must implement. Subclass
6
+ # this in the host application to wire additional providers
7
+ # (Apple Sign In, Microsoft, GitLab, etc.) and register the
8
+ # subclass in `Auth.configuration.oauth_providers`.
9
+ #
10
+ # The Auth engine ships two concrete adapters: Auth::OAuth::Google
11
+ # and Auth::OAuth::Github. See those files for fully-worked
12
+ # examples + the docs URLs each was verified against.
13
+ #
14
+ # All adapters are stateless. Build them per-request via
15
+ # +Auth::OAuth.build(:provider_name)+ which reads credentials from
16
+ # +Auth.configuration.oauth_providers+.
17
+ class Abstract
18
+ # Returned by #fetch_user_info — the normalised user representation
19
+ # downstream code uses to find-or-create the local OAuth row.
20
+ Profile = Struct.new(:provider_uid, :email, :email_verified, :name, :avatar_url, :raw,
21
+ keyword_init: true)
22
+
23
+ def initialize(client_id:, client_secret:, scopes: [])
24
+ @client_id = client_id
25
+ @client_secret = client_secret
26
+ @scopes = scopes
27
+ end
28
+
29
+ # The URL the host redirects the user to. State is the
30
+ # CSRF-protection token the host generates and re-verifies on
31
+ # callback.
32
+ def authorize_url(state:, redirect_uri:)
33
+ raise NotImplementedError, "#{self.class} must implement #authorize_url"
34
+ end
35
+
36
+ # Exchange the `code` query param from the callback for an
37
+ # access token. Returns a hash with keys :access_token,
38
+ # :refresh_token (may be nil), :expires_in (may be nil),
39
+ # :token_type, :scope.
40
+ def exchange_code(code:, redirect_uri:)
41
+ raise NotImplementedError, "#{self.class} must implement #exchange_code"
42
+ end
43
+
44
+ # Fetch the user's profile from the provider's userinfo
45
+ # endpoint. Returns a Profile struct.
46
+ def fetch_user_info(access_token:)
47
+ raise NotImplementedError, "#{self.class} must implement #fetch_user_info"
48
+ end
49
+
50
+ protected
51
+
52
+ attr_reader :client_id, :client_secret, :scopes
53
+
54
+ # Single Faraday connection per adapter instance. Subclasses set
55
+ # the base URL; we share timeout + retry config across providers.
56
+ def conn(base_url)
57
+ Faraday.new(url: base_url) do |f|
58
+ f.request :url_encoded
59
+ f.options.timeout = 10
60
+ f.options.open_timeout = 5
61
+ f.adapter Faraday.default_adapter
62
+ end
63
+ end
64
+
65
+ def parse_json(response, action:)
66
+ return {} if response.body.to_s.empty?
67
+
68
+ JSON.parse(response.body)
69
+ rescue JSON::ParserError => e
70
+ raise Auth::OAuthError,
71
+ "OAuth #{provider_name} #{action}: response was not valid JSON (#{e.message})"
72
+ end
73
+
74
+ def assert_success!(response, action:)
75
+ return if response.success?
76
+
77
+ body = response.body.to_s[0, 200]
78
+ raise Auth::OAuthError,
79
+ "OAuth #{provider_name} #{action}: HTTP #{response.status} — #{body}"
80
+ end
81
+
82
+ def provider_name
83
+ self.class.name.to_s.split("::").last.downcase
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "auth/oauth/abstract"
4
+
5
+ module Auth
6
+ module OAuth
7
+ # GitHub OAuth Apps adapter. Faraday-based.
8
+ #
9
+ # Verified against:
10
+ # https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
11
+ # https://docs.github.com/en/rest/users/users
12
+ # https://docs.github.com/en/rest/users/emails
13
+ #
14
+ # GitHub returns form-encoded responses by default — we send
15
+ # `Accept: application/json` to get JSON consistently.
16
+ #
17
+ # The `/user` endpoint returns null email when the user keeps it
18
+ # private. We additionally fetch `/user/emails` (which requires
19
+ # the `user:email` scope) and pick the primary verified one. If
20
+ # `user:email` isn't in scopes, we fall back to whatever `/user`
21
+ # returns (which may be nil — caller should handle).
22
+ class Github < Abstract
23
+ AUTHORIZE_URL = "https://github.com/login/oauth/authorize"
24
+ TOKEN_URL = "https://github.com/login/oauth/access_token"
25
+ USER_URL = "https://api.github.com/user"
26
+ USER_EMAILS_URL = "https://api.github.com/user/emails"
27
+
28
+ DEFAULT_SCOPES = %w[read:user user:email].freeze
29
+
30
+ def initialize(client_id:, client_secret:, scopes: DEFAULT_SCOPES)
31
+ super
32
+ end
33
+
34
+ def authorize_url(state:, redirect_uri:)
35
+ params = {
36
+ client_id: client_id,
37
+ redirect_uri: redirect_uri,
38
+ scope: scopes.join(" "),
39
+ state: state,
40
+ allow_signup: "true"
41
+ }
42
+ "#{AUTHORIZE_URL}?#{URI.encode_www_form(params)}"
43
+ end
44
+
45
+ def exchange_code(code:, redirect_uri:)
46
+ response = conn(TOKEN_URL).post("") do |req|
47
+ req.headers["Accept"] = "application/json"
48
+ req.body = URI.encode_www_form(
49
+ client_id: client_id,
50
+ client_secret: client_secret,
51
+ code: code,
52
+ redirect_uri: redirect_uri
53
+ )
54
+ end
55
+ assert_success!(response, action: "token exchange")
56
+ body = parse_json(response, action: "token exchange")
57
+ # GitHub returns 200 with { "error": "bad_verification_code" }
58
+ # for bad codes — treat that as a failure too.
59
+ raise Auth::OAuthError, "OAuth github token exchange: #{body["error_description"] || body["error"]}" if body["error"]
60
+
61
+ {
62
+ access_token: body["access_token"],
63
+ refresh_token: nil, # GitHub OAuth Apps don't issue refresh tokens
64
+ expires_in: nil, # tokens don't expire (GitHub OAuth Apps)
65
+ token_type: body["token_type"] || "bearer",
66
+ scope: body["scope"]
67
+ }
68
+ end
69
+
70
+ def fetch_user_info(access_token:)
71
+ user = fetch_user(access_token)
72
+ email = user["email"] || best_email(access_token)
73
+
74
+ Profile.new(
75
+ provider_uid: user["id"].to_s, # GitHub numeric id, stable
76
+ email: email,
77
+ email_verified: !email.nil?, # /user/emails primary+verified is what we picked
78
+ name: user["name"] || user["login"],
79
+ avatar_url: user["avatar_url"],
80
+ raw: user
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ def fetch_user(access_token)
87
+ response = conn(USER_URL).get("") do |req|
88
+ req.headers["Authorization"] = "Bearer #{access_token}"
89
+ req.headers["Accept"] = "application/vnd.github+json"
90
+ req.headers["User-Agent"] = "Seams Auth"
91
+ end
92
+ assert_success!(response, action: "user fetch")
93
+ parse_json(response, action: "user fetch")
94
+ end
95
+
96
+ def best_email(access_token)
97
+ return nil unless scopes.include?("user:email")
98
+
99
+ response = conn(USER_EMAILS_URL).get("") do |req|
100
+ req.headers["Authorization"] = "Bearer #{access_token}"
101
+ req.headers["Accept"] = "application/vnd.github+json"
102
+ req.headers["User-Agent"] = "Seams Auth"
103
+ end
104
+ return nil unless response.success?
105
+
106
+ emails = JSON.parse(response.body)
107
+ primary = emails.find { |e| e["primary"] && e["verified"] }
108
+ primary&.dig("email")
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "auth/oauth/abstract"
4
+
5
+ module Auth
6
+ module OAuth
7
+ # Google OAuth 2.0 + OpenID Connect adapter. Faraday-based
8
+ # (no oauth2 gem, no Net::HTTP).
9
+ #
10
+ # Verified against:
11
+ # https://developers.google.com/identity/protocols/oauth2/web-server
12
+ # https://developers.google.com/identity/openid-connect/openid-connect
13
+ #
14
+ # Default scopes (`openid email profile`) cover the four fields we
15
+ # populate on Profile. Use `access_type=offline` + `prompt=consent`
16
+ # only if you need refresh tokens — most "sign in with Google"
17
+ # flows don't.
18
+ class Google < Abstract
19
+ AUTHORIZE_URL = "https://accounts.google.com/o/oauth2/v2/auth"
20
+ TOKEN_URL = "https://oauth2.googleapis.com/token"
21
+ USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
22
+
23
+ DEFAULT_SCOPES = %w[openid email profile].freeze
24
+
25
+ def initialize(client_id:, client_secret:, scopes: DEFAULT_SCOPES)
26
+ super
27
+ end
28
+
29
+ def authorize_url(state:, redirect_uri:)
30
+ params = {
31
+ client_id: client_id,
32
+ redirect_uri: redirect_uri,
33
+ response_type: "code",
34
+ scope: scopes.join(" "),
35
+ state: state,
36
+ access_type: "online",
37
+ prompt: "select_account"
38
+ }
39
+ "#{AUTHORIZE_URL}?#{URI.encode_www_form(params)}"
40
+ end
41
+
42
+ def exchange_code(code:, redirect_uri:)
43
+ response = conn(TOKEN_URL).post("", {
44
+ client_id: client_id,
45
+ client_secret: client_secret,
46
+ code: code,
47
+ redirect_uri: redirect_uri,
48
+ grant_type: "authorization_code"
49
+ })
50
+ assert_success!(response, action: "token exchange")
51
+ body = parse_json(response, action: "token exchange")
52
+ {
53
+ access_token: body["access_token"],
54
+ refresh_token: body["refresh_token"],
55
+ expires_in: body["expires_in"],
56
+ token_type: body["token_type"] || "Bearer",
57
+ scope: body["scope"]
58
+ }
59
+ end
60
+
61
+ def fetch_user_info(access_token:)
62
+ response = conn(USERINFO_URL).get("") do |req|
63
+ req.headers["Authorization"] = "Bearer #{access_token}"
64
+ end
65
+ assert_success!(response, action: "userinfo")
66
+ body = parse_json(response, action: "userinfo")
67
+ Profile.new(
68
+ provider_uid: body["sub"], # stable Google account id
69
+ email: body["email"],
70
+ email_verified: body["email_verified"],
71
+ name: body["name"],
72
+ avatar_url: body["picture"],
73
+ raw: body
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Re-encrypts plaintext PII columns for hosts upgrading from Auth
4
+ # generator versions ≤ Wave 10 (where `email` and `provider_uid` were
5
+ # stored plaintext).
6
+ #
7
+ # Usage on the host:
8
+ #
9
+ # 1. Set the transitional flag in config/application.rb so existing
10
+ # plaintext rows can still be read while the rotation runs:
11
+ #
12
+ # config.active_record.encryption.support_unencrypted_data = true
13
+ #
14
+ # 2. Deploy + run:
15
+ #
16
+ # bin/rails seams:auth:rotate_pii_encryption
17
+ #
18
+ # 3. Once the task reports zero remaining unencrypted rows, set the
19
+ # flag back to `false` (default) and redeploy.
20
+ #
21
+ # Idempotent — re-running on already-encrypted rows is a no-op because
22
+ # the assignment goes through the encryption setter every time.
23
+ namespace :seams do
24
+ namespace :auth do
25
+ desc "Re-encrypt PII columns (email, provider_uid) — host upgrades from Wave ≤10"
26
+ task rotate_pii_encryption: :environment do
27
+ counts = { identities: 0, oauth_providers: 0 }
28
+ failures = { identities: [], oauth_providers: [] }
29
+
30
+ # `update!` runs ALL validations. Legacy rows whose data fails
31
+ # today's regex (e.g. emails without @, stale imports) would
32
+ # raise RecordInvalid mid-`find_each` and abort the rotation,
33
+ # leaving downstream rows plaintext. Trap per-row, log, count
34
+ # the failure, and keep going — the operator gets a final report
35
+ # of which rows still need attention before flipping
36
+ # support_unencrypted_data back to false.
37
+ Auth::Identity.find_each do |identity|
38
+ identity.update!(email: identity.email)
39
+ counts[:identities] += 1
40
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
41
+ failures[:identities] << { id: identity.id, error: "#{e.class}: #{e.message}" }
42
+ end
43
+
44
+ Auth::OAuth::Provider.find_each do |provider|
45
+ provider.update!(provider_uid: provider.provider_uid)
46
+ counts[:oauth_providers] += 1
47
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique => e
48
+ failures[:oauth_providers] << { id: provider.id, error: "#{e.class}: #{e.message}" }
49
+ end
50
+
51
+ puts "Auth PII rotation complete:"
52
+ puts " identities re-encrypted: #{counts[:identities]}, failures: #{failures[:identities].size}"
53
+ puts " oauth_providers re-encrypted: #{counts[:oauth_providers]}, failures: #{failures[:oauth_providers].size}"
54
+
55
+ if failures.values.any?(&:any?)
56
+ puts ""
57
+ puts "Failures (fix the underlying data, then re-run this task):"
58
+ failures.each do |table, rows|
59
+ rows.each { |row| puts " #{table} id=#{row[:id]} — #{row[:error]}" }
60
+ end
61
+ puts ""
62
+ puts "DO NOT set support_unencrypted_data = false until failure count is zero — " \
63
+ "those rows are still plaintext."
64
+ abort("Rotation incomplete: #{failures.values.flatten.size} row(s) failed.")
65
+ end
66
+ end
67
+ end
68
+ end