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