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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +335 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
- data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
- data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
- data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
- data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
- data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
- data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
- data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
- data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
- data/lib/generators/seams/admin/admin_generator.rb +852 -0
- data/lib/generators/seams/admin/templates/README.md.tt +266 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
- data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
- data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
- data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
- data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
- data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
- data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
- data/lib/generators/seams/auth/auth_generator.rb +311 -0
- data/lib/generators/seams/auth/templates/README.md.tt +289 -0
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
- data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
- data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
- data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
- data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
- data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
- data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
- data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
- data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
- data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
- data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
- data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
- data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
- data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
- data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
- data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
- data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
- data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
- data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
- data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
- data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
- data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
- data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
- data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
- data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
- data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
- data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
- data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
- data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
- data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
- data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
- data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
- data/lib/generators/seams/billing/billing_generator.rb +476 -0
- data/lib/generators/seams/billing/templates/README.md.tt +355 -0
- data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
- data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
- data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
- data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
- data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
- data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
- data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
- data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
- data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
- data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
- data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
- data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
- data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
- data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
- data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
- data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
- data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
- data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
- data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
- data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
- data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
- data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
- data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
- data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
- data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
- data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
- data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
- data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
- data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
- data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
- data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
- data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
- data/lib/generators/seams/core/core_generator.rb +191 -0
- data/lib/generators/seams/core/templates/README.md.tt +45 -0
- data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
- data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
- data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
- data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
- data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
- data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
- data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
- data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
- data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
- data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
- data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
- data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
- data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
- data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
- data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
- data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
- data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
- data/lib/generators/seams/engine/engine_generator.rb +165 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
- data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
- data/lib/generators/seams/engine/templates/README.md.tt +40 -0
- data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
- data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
- data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
- data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
- data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
- data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
- data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
- data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
- data/lib/generators/seams/install/install_generator.rb +211 -0
- data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
- data/lib/generators/seams/install/templates/Procfile.tt +14 -0
- data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
- data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
- data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
- data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
- data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
- data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
- data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
- data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
- data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
- data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
- data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
- data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
- data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
- data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
- data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
- data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
- data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
- data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
- data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
- data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
- data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
- data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
- data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
- data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
- data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
- data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
- data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
- data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
- data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
- data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
- data/lib/generators/seams/remove/remove_generator.rb +259 -0
- data/lib/generators/seams/teams/teams_generator.rb +298 -0
- data/lib/generators/seams/teams/templates/README.md.tt +88 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
- data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
- data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
- data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
- data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
- data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
- data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
- data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
- data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
- data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
- data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
- data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
- data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
- data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
- data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
- data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
- data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
- data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
- data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
- data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
- data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
- data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
- data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
- data/lib/seams/cli/list.rb +111 -0
- data/lib/seams/cli/quality.rb +99 -0
- data/lib/seams/cli/resolve.rb +276 -0
- data/lib/seams/cli/test_changed.rb +116 -0
- data/lib/seams/cli.rb +48 -0
- data/lib/seams/configuration.rb +19 -0
- data/lib/seams/cops/known_queue_names.rb +42 -0
- data/lib/seams/cops/migration_comments.rb +68 -0
- data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
- data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
- data/lib/seams/cops.rb +18 -0
- data/lib/seams/event_registry.rb +49 -0
- data/lib/seams/events/adapter.rb +24 -0
- data/lib/seams/events/adapters/active_support.rb +31 -0
- data/lib/seams/events/publisher.rb +178 -0
- data/lib/seams/events.rb +39 -0
- data/lib/seams/generators/dummy_app_writer.rb +424 -0
- data/lib/seams/generators/eject_aware.rb +102 -0
- data/lib/seams/generators/follow_up_generator.rb +148 -0
- data/lib/seams/generators/host_injector.rb +124 -0
- data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
- data/lib/seams/generators/splicer.rb +217 -0
- data/lib/seams/observability/adapter.rb +33 -0
- data/lib/seams/observability/adapters/rails_logger.rb +59 -0
- data/lib/seams/observability.rb +34 -0
- data/lib/seams/runtime.rb +23 -0
- data/lib/seams/version.rb +5 -0
- data/lib/seams.rb +23 -0
- 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
|