regstack 0.6.0__tar.gz → 0.8.0__tar.gz
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.
- {regstack-0.6.0 → regstack-0.8.0}/CHANGELOG.md +214 -0
- {regstack-0.6.0 → regstack-0.8.0}/PKG-INFO +3 -3
- {regstack-0.6.0 → regstack-0.8.0}/README.md +1 -1
- {regstack-0.6.0 → regstack-0.8.0}/pyproject.toml +33 -4
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/app.py +54 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/dependencies.py +36 -1
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/rate_limit.py +2 -0
- regstack-0.8.0/src/regstack/backends/mongo/indexes.py +192 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/user_repo.py +18 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/migrations/versions/0002_oauth.py +23 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/__main__.py +54 -15
- regstack-0.8.0/src/regstack/cli/_results.py +29 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/doctor.py +2 -9
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/init.py +6 -21
- regstack-0.8.0/src/regstack/cli/validate/__init__.py +9 -0
- regstack-0.8.0/src/regstack/cli/validate/capture.py +56 -0
- regstack-0.8.0/src/regstack/cli/validate/cli.py +326 -0
- regstack-0.8.0/src/regstack/cli/validate/http.py +126 -0
- regstack-0.8.0/src/regstack/cli/validate/logtail.py +276 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/__init__.py +5 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/account.py +148 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/cleanup.py +71 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/core_auth.py +171 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/feature_discover.py +68 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/oauth.py +57 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/password_reset.py +99 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/reachability.py +50 -0
- regstack-0.8.0/src/regstack/cli/validate/phases/sms_mfa.py +123 -0
- regstack-0.8.0/src/regstack/cli/validate/report.py +33 -0
- regstack-0.8.0/src/regstack/cli/validate/runner.py +129 -0
- regstack-0.8.0/src/regstack/config/schema.py +373 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/composer.py +3 -1
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/console.py +8 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/factory.py +1 -1
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/ses.py +23 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/user.py +10 -4
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/providers/google.py +1 -1
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/account.py +1 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/admin.py +31 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/oauth.py +19 -1
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/password.py +1 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/register.py +1 -2
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/verify.py +16 -3
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/sms/factory.py +1 -1
- regstack-0.8.0/src/regstack/sms/null.py +38 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/sms/sns.py +1 -1
- regstack-0.8.0/src/regstack/version.py +1 -0
- regstack-0.8.0/src/regstack/wizard/ses/__init__.py +5 -0
- regstack-0.8.0/src/regstack/wizard/ses/_aws.py +315 -0
- regstack-0.8.0/src/regstack/wizard/ses/cli.py +224 -0
- regstack-0.8.0/src/regstack/wizard/ses/routes.py +367 -0
- regstack-0.8.0/src/regstack/wizard/ses/server.py +87 -0
- regstack-0.8.0/src/regstack/wizard/ses/static/wizard.css +125 -0
- regstack-0.8.0/src/regstack/wizard/ses/static/wizard.js +266 -0
- regstack-0.8.0/src/regstack/wizard/ses/templates/wizard.html +25 -0
- regstack-0.8.0/src/regstack/wizard/ses/validators.py +319 -0
- regstack-0.8.0/src/regstack/wizard/ses/window.py +59 -0
- regstack-0.8.0/src/regstack/wizard/ses/writer.py +267 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/templates/designer.html +2 -2
- regstack-0.6.0/.github/workflows/publish.yml +0 -82
- regstack-0.6.0/.github/workflows/test.yml +0 -161
- regstack-0.6.0/.python-version +0 -1
- regstack-0.6.0/.readthedocs.yaml +0 -23
- regstack-0.6.0/CLAUDE.md +0 -358
- regstack-0.6.0/docs/_static/.gitkeep +0 -0
- regstack-0.6.0/docs/_templates/.gitkeep +0 -0
- regstack-0.6.0/docs/api.md +0 -351
- regstack-0.6.0/docs/architecture.md +0 -250
- regstack-0.6.0/docs/changelog.md +0 -783
- regstack-0.6.0/docs/cli.md +0 -187
- regstack-0.6.0/docs/conf.py +0 -98
- regstack-0.6.0/docs/configuration.md +0 -425
- regstack-0.6.0/docs/embedding.md +0 -252
- regstack-0.6.0/docs/index.md +0 -197
- regstack-0.6.0/docs/oauth.md +0 -154
- regstack-0.6.0/docs/quickstart.md +0 -162
- regstack-0.6.0/docs/security-reports/README.md +0 -15
- regstack-0.6.0/docs/security.md +0 -328
- regstack-0.6.0/docs/theming.md +0 -213
- regstack-0.6.0/examples/_common/app.py +0 -101
- regstack-0.6.0/examples/mongo/README.md +0 -30
- regstack-0.6.0/examples/mongo/branding/theme.css +0 -25
- regstack-0.6.0/examples/mongo/main.py +0 -31
- regstack-0.6.0/examples/mongo/regstack.toml +0 -30
- regstack-0.6.0/examples/postgres/README.md +0 -29
- regstack-0.6.0/examples/postgres/main.py +0 -30
- regstack-0.6.0/examples/postgres/regstack.toml +0 -29
- regstack-0.6.0/examples/sqlite/README.md +0 -33
- regstack-0.6.0/examples/sqlite/main.py +0 -34
- regstack-0.6.0/examples/sqlite/regstack.toml +0 -30
- regstack-0.6.0/scripts/ccr_coverage_setup.py +0 -242
- regstack-0.6.0/scripts/security-review-prompt.md +0 -583
- regstack-0.6.0/src/regstack/backends/mongo/indexes.py +0 -98
- regstack-0.6.0/src/regstack/cli/__init__.py +0 -0
- regstack-0.6.0/src/regstack/config/schema.py +0 -208
- regstack-0.6.0/src/regstack/sms/null.py +0 -26
- regstack-0.6.0/src/regstack/version.py +0 -1
- regstack-0.6.0/tasks/oauth-design.md +0 -729
- regstack-0.6.0/tasks.py +0 -415
- regstack-0.6.0/tests/__init__.py +0 -0
- regstack-0.6.0/tests/_fake_google/__init__.py +0 -14
- regstack-0.6.0/tests/_fake_google/provider.py +0 -166
- regstack-0.6.0/tests/conftest.py +0 -266
- regstack-0.6.0/tests/e2e/__init__.py +0 -0
- regstack-0.6.0/tests/e2e/conftest.py +0 -121
- regstack-0.6.0/tests/e2e/test_theme_designer.py +0 -87
- regstack-0.6.0/tests/e2e/test_wizard_oauth_flow.py +0 -144
- regstack-0.6.0/tests/integration/__init__.py +0 -0
- regstack-0.6.0/tests/integration/test_account_management.py +0 -314
- regstack-0.6.0/tests/integration/test_admin_router.py +0 -301
- regstack-0.6.0/tests/integration/test_happy_path.py +0 -174
- regstack-0.6.0/tests/integration/test_indexes.py +0 -43
- regstack-0.6.0/tests/integration/test_login_lockout.py +0 -82
- regstack-0.6.0/tests/integration/test_mfa.py +0 -353
- regstack-0.6.0/tests/integration/test_oauth_google_router.py +0 -735
- regstack-0.6.0/tests/integration/test_oauth_repos.py +0 -267
- regstack-0.6.0/tests/integration/test_oauth_ui.py +0 -168
- regstack-0.6.0/tests/integration/test_password_reset.py +0 -121
- regstack-0.6.0/tests/integration/test_rate_limits.py +0 -215
- regstack-0.6.0/tests/integration/test_sql_migrations.py +0 -89
- regstack-0.6.0/tests/integration/test_ui_router.py +0 -117
- regstack-0.6.0/tests/integration/test_verification.py +0 -143
- regstack-0.6.0/tests/unit/__init__.py +0 -0
- regstack-0.6.0/tests/unit/test_base_install_imports.py +0 -79
- regstack-0.6.0/tests/unit/test_cli.py +0 -133
- regstack-0.6.0/tests/unit/test_cli_doctor.py +0 -164
- regstack-0.6.0/tests/unit/test_cli_init.py +0 -150
- regstack-0.6.0/tests/unit/test_cli_migrate.py +0 -151
- regstack-0.6.0/tests/unit/test_cli_wizard_missing_extra.py +0 -59
- regstack-0.6.0/tests/unit/test_config_loader.py +0 -41
- regstack-0.6.0/tests/unit/test_jwt.py +0 -61
- regstack-0.6.0/tests/unit/test_lockout.py +0 -101
- regstack-0.6.0/tests/unit/test_mail_composer.py +0 -88
- regstack-0.6.0/tests/unit/test_mfa_code_repo.py +0 -126
- regstack-0.6.0/tests/unit/test_oauth_google.py +0 -445
- regstack-0.6.0/tests/unit/test_password.py +0 -16
- regstack-0.6.0/tests/unit/test_ses_backend.py +0 -26
- regstack-0.6.0/tests/unit/test_sms.py +0 -77
- regstack-0.6.0/tests/unit/test_smtp_backend.py +0 -62
- regstack-0.6.0/tests/unit/test_theme_designer_cli.py +0 -190
- regstack-0.6.0/tests/unit/test_theme_designer_routes.py +0 -179
- regstack-0.6.0/tests/unit/test_theme_designer_validators.py +0 -154
- regstack-0.6.0/tests/unit/test_theme_designer_writer.py +0 -156
- regstack-0.6.0/tests/unit/test_ui_env.py +0 -51
- regstack-0.6.0/tests/unit/test_wizard_oauth_cli.py +0 -230
- regstack-0.6.0/tests/unit/test_wizard_oauth_routes.py +0 -232
- regstack-0.6.0/tests/unit/test_wizard_oauth_validators.py +0 -257
- regstack-0.6.0/tests/unit/test_wizard_oauth_writer.py +0 -293
- regstack-0.6.0/uv.lock +0 -3314
- {regstack-0.6.0 → regstack-0.8.0}/.gitignore +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/LICENSE +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/NOTICE +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/SECURITY.md +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/regstack.toml.example +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/clock.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/jwt.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/lockout.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/mfa.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/password.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/auth/tokens.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/base.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/factory.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/backend.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/client.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/oauth_state_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/protocols.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/backend.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/migrations/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/migrations/env.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
- {regstack-0.6.0/examples/_common → regstack-0.8.0/src/regstack/backends/sql/repositories}/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/oauth_identity_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/oauth_state_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/schema.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/backends/sql/types.py +0 -0
- {regstack-0.6.0/src/regstack/backends/sql/repositories → regstack-0.8.0/src/regstack/cli}/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/_runtime.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/admin.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/cli/migrate.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/config/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/config/loader.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/config/secrets.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/base.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/smtp.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/email_change.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/email_change.subject.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/email_change.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/password_reset.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/password_reset.subject.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/password_reset.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/verification.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/verification.subject.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/email/templates/verification.txt +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/hooks/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/hooks/events.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/_objectid.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/login_attempt.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/mfa_code.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/oauth_identity.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/oauth_state.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/models/pending_registration.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/base.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/errors.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/providers/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/oauth/registry.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/_helpers.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/_schemas.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/login.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/logout.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/routers/phone.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/sms/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/sms/base.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/sms/twilio.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/pages.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/static/css/core.css +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/static/css/theme.css +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/static/js/regstack.js +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/forgot.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/login.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/me.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/oauth_complete.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/register.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/reset.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/auth/verify.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/ui/templates/base.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/cli.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/routes.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/server.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/static/wizard.css +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/static/wizard.js +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/templates/wizard.html +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/validators.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/window.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/oauth_google/writer.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/__init__.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/cli.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/routes.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/server.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/static/designer.css +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/static/designer.js +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/validators.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/window.py +0 -0
- {regstack-0.6.0 → regstack-0.8.0}/src/regstack/wizard/theme_designer/writer.py +0 -0
|
@@ -5,6 +5,220 @@ authoritative copy lives at
|
|
|
5
5
|
[`docs/changelog.md`](docs/changelog.md) and is rendered into the
|
|
6
6
|
Sphinx docs.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.8.0 — 2026-05-19
|
|
11
|
+
|
|
12
|
+
`regstack ses setup` guided wizard, plus two security fixes from
|
|
13
|
+
the 2026-05-18 daily review.
|
|
14
|
+
|
|
15
|
+
**Added: `regstack ses setup`.** A pywebview wizard for the SES
|
|
16
|
+
email backend, mirroring the existing `regstack oauth setup` flow.
|
|
17
|
+
Nine steps walk through region selection, credential source
|
|
18
|
+
(`profile` / `explicit` / `chain`), sender-domain identity
|
|
19
|
+
verification (via SES `GetIdentityVerificationAttributes`),
|
|
20
|
+
sandbox detection (via `GetAccount` with `GetSendQuota` heuristic
|
|
21
|
+
fallback for IAM-restricted policies), and a live test send.
|
|
22
|
+
Non-clobbering tomlkit + secrets.env merge. Headless
|
|
23
|
+
`--print-only` mode for CI / scripting. Gated behind the joint
|
|
24
|
+
extra: `pip install 'regstack[wizard,ses]'`.
|
|
25
|
+
|
|
26
|
+
**Fixed: theme-designer preview no longer ships well-known credentials
|
|
27
|
+
in the wheel.** `designer.html` had `alice@example.com` /
|
|
28
|
+
`hunter2hunter2` as `value=` attributes on its login-form preview;
|
|
29
|
+
flipped to `placeholder=` so the wheel doesn't carry well-known
|
|
30
|
+
example creds that could be mistaken for real fixtures.
|
|
31
|
+
(Daily security review 2026-05-18 · I-1.)
|
|
32
|
+
|
|
33
|
+
**Fixed: Google OAuth token-exchange error no longer echoes the
|
|
34
|
+
response body.** `exchange_code()` previously raised
|
|
35
|
+
`OAuthTokenExchangeError(f"... {body!r}")` on the rare 200-without-id_token
|
|
36
|
+
edge case. The body can contain a live short-lived `access_token` in
|
|
37
|
+
that path, and the OAuth router logs the exception text at WARNING.
|
|
38
|
+
Dropped `{body!r}` from the message; regression test in
|
|
39
|
+
`tests/unit/test_oauth_google.py` pins that a planted token never
|
|
40
|
+
appears in the exception's `str()` or `args`.
|
|
41
|
+
(Daily security review 2026-05-18 · I-2.)
|
|
42
|
+
|
|
43
|
+
## 0.7.0 — 2026-05-17
|
|
44
|
+
|
|
45
|
+
Two-week sprint that lands the `regstack validate` end-to-end probe,
|
|
46
|
+
seven security-review findings, a clutch of host-integration
|
|
47
|
+
ergonomic wins (per-link email URL templates, optional auth
|
|
48
|
+
dependency, admin `promote_pending`, explicit SES credentials), and
|
|
49
|
+
two breaking API trims (`UserPublic._id` → `id`,
|
|
50
|
+
`TokenTransport = "bearer"` only).
|
|
51
|
+
|
|
52
|
+
The headline is `regstack validate` — a new CLI command that drives
|
|
53
|
+
a real deployed install through every auth flow (register, verify,
|
|
54
|
+
login, logout, password reset, change-email, OAuth start, SMS 2FA)
|
|
55
|
+
from a remote operator workstation, scraping one-time tokens out of
|
|
56
|
+
the deployment's stdout via a `--log-source` of your choice
|
|
57
|
+
(`file:`, `ssh:`, `docker:`, `cmd:`). The companion to `regstack
|
|
58
|
+
doctor`: doctor checks the loaded config, validate checks the
|
|
59
|
+
running service.
|
|
60
|
+
|
|
61
|
+
**Breaking.**
|
|
62
|
+
|
|
63
|
+
- **`UserPublic` JSON key is `id`, not `_id`.** The `alias="_id"`
|
|
64
|
+
on `UserPublic.id` (and the accompanying `populate_by_name=True`)
|
|
65
|
+
is removed. Every endpoint returning a `UserPublic` —
|
|
66
|
+
`POST /api/auth/register`, `GET /api/auth/me`, `PATCH /api/auth/me`,
|
|
67
|
+
and the admin user endpoints — now sends `id` on the wire.
|
|
68
|
+
`BaseUser` (the Mongo-document model) keeps the alias because it
|
|
69
|
+
round-trips to BSON via `to_mongo()`; only the API contract is
|
|
70
|
+
touched. Clients that read `body["_id"]` should switch to
|
|
71
|
+
`body["id"]`. Hosts hand-rolling a `/me` override solely to swap
|
|
72
|
+
the key shape can drop it.
|
|
73
|
+
- **`TokenTransport` literal narrowed to `Literal["bearer"]`.**
|
|
74
|
+
`"cookie"` was previously accepted by config validation but
|
|
75
|
+
silently no-op'd (no router ever set `Set-Cookie`). Hosts that
|
|
76
|
+
set `transport = "cookie"` now get a clear pydantic
|
|
77
|
+
`literal_error` at startup instead of a silent
|
|
78
|
+
security-misconfiguration. `RegStackConfig.cookie_domain` is
|
|
79
|
+
removed along with it. `regstack init` no longer offers the
|
|
80
|
+
cookie option either.
|
|
81
|
+
|
|
82
|
+
**Added.**
|
|
83
|
+
|
|
84
|
+
- **`regstack validate`.** End-to-end probe of a deployed install
|
|
85
|
+
— registers a throwaway user, walks every auth flow, then
|
|
86
|
+
deletes it. Reads one-time tokens out of the deployment's
|
|
87
|
+
stdout via `--log-source` (file / ssh / docker / arbitrary
|
|
88
|
+
command). Skip phases with `--skip`. Companion to `regstack
|
|
89
|
+
doctor` (which only validates loaded config). See
|
|
90
|
+
`regstack validate --help` for the full operator runbook.
|
|
91
|
+
- **`email.log_bodies` and `sms.log_bodies` config flags** to
|
|
92
|
+
promote the console / null backends' body log lines from
|
|
93
|
+
DEBUG → INFO without enabling DEBUG globally. `email.log_bodies`
|
|
94
|
+
defaults to `False`; `sms.log_bodies` defaults to `True`
|
|
95
|
+
(preserves prior null-SMS behaviour). Other backends ignore.
|
|
96
|
+
- **`RegStackConfig.email_link_prefix` + auto-resolve from
|
|
97
|
+
`ui_prefix`.** Verification / reset / email-change links now
|
|
98
|
+
default to `<base_url><ui_prefix>/verify?token=...` when the
|
|
99
|
+
bundled UI router is enabled, instead of bare `/verify`. Hosts
|
|
100
|
+
whose SPA owns the auth pages can pin a path explicitly via
|
|
101
|
+
`email_link_prefix`; the bundled UI hosts get the right links
|
|
102
|
+
automatically.
|
|
103
|
+
- **`EmailConfig.from_name` defaults to `app_name`** when unset.
|
|
104
|
+
Hosts that change `app_name` to brand outgoing email also get
|
|
105
|
+
the matching `From:` header automatically. Explicit `from_name`
|
|
106
|
+
values still win.
|
|
107
|
+
- **Per-link email URL templates.** Three new optional fields on
|
|
108
|
+
`RegStackConfig` — `verify_url_template`,
|
|
109
|
+
`password_reset_url_template`, `email_change_url_template` —
|
|
110
|
+
let SPAs whose router shape doesn't fit
|
|
111
|
+
`/verify?token=...` rewrite the email links. Templates
|
|
112
|
+
substitute `{base_url}` and `{token}` literally. Hash-routed
|
|
113
|
+
SPA: `"{base_url}/#/verify/{token}"`. Sibling subdomain:
|
|
114
|
+
`"https://auth.example.com/verify/{token}"`. Default unset
|
|
115
|
+
falls back to the prefix-based composition above. New helpers
|
|
116
|
+
`RegStackConfig.resolve_{verify,password_reset,email_change}_url(token)`.
|
|
117
|
+
- **`current_user_optional` dependency.** Companion to
|
|
118
|
+
`current_user` / `current_admin` on `regstack.deps`. Returns
|
|
119
|
+
`BaseUser | None` instead of raising 401, for endpoints that
|
|
120
|
+
render differently for signed-in vs anonymous callers (cart
|
|
121
|
+
icon, comment-author prefill). Every form of auth failure —
|
|
122
|
+
missing header, wrong scheme, malformed / expired / revoked
|
|
123
|
+
token, deleted or bulk-revoked user — collapses to `None`.
|
|
124
|
+
- **`RegStack.promote_pending(email)` + admin route.** Converts
|
|
125
|
+
a `PendingRegistration` row directly into a verified active
|
|
126
|
+
user, bypassing the email-link round-trip. Hashed password and
|
|
127
|
+
full name carry over verbatim. Fires the same `user_verified`
|
|
128
|
+
hook as `POST /verify`. Useful for admin rescue of stuck
|
|
129
|
+
signups, batch seeding from a known-good list, and dev
|
|
130
|
+
fixtures. Exposed as `POST /admin/pending/{email}/promote`
|
|
131
|
+
when the admin router is enabled.
|
|
132
|
+
- **Explicit SES credential fields on `EmailConfig`.** New
|
|
133
|
+
`ses_access_key_id` / `ses_secret_access_key` (both `SecretStr |
|
|
134
|
+
None`) let hosts pass AWS creds directly instead of relying on
|
|
135
|
+
boto3's env-var fallthrough. Validated as a pair, mutually
|
|
136
|
+
exclusive with `ses_profile`.
|
|
137
|
+
|
|
138
|
+
**Security.**
|
|
139
|
+
|
|
140
|
+
- **CVE-2026-42561 — `python-multipart>=0.0.27`.** Closes a
|
|
141
|
+
network-exploitable DoS via unbounded multipart part-header
|
|
142
|
+
parsing (CVSS 7.5). Previous floor `>=0.0.26` had the earlier
|
|
143
|
+
CVE-2026-40347 fix only.
|
|
144
|
+
- **sdist no longer ships internal docs to PyPI.** Added a
|
|
145
|
+
`[tool.hatch.build.targets.sdist]` exclude block. The published
|
|
146
|
+
source tarball used to contain `CLAUDE.md` (with a developer
|
|
147
|
+
home-directory path), the security-review prompt, the full test
|
|
148
|
+
suite, build tooling, and (when built from a worktree) a `.git`
|
|
149
|
+
text file pointing at the operator's worktrees directory.
|
|
150
|
+
- **Defensive `ObjectId.is_valid()` on nine Mongo UserRepo
|
|
151
|
+
mutations.** `set_last_login`, `set_tokens_invalidated_after`,
|
|
152
|
+
`update_password`, `set_active`, `set_superuser`, `set_full_name`,
|
|
153
|
+
`set_phone`, `set_mfa_enabled`, and `update_email` now match
|
|
154
|
+
`get_by_id` / `delete`: invalid input no-ops instead of raising
|
|
155
|
+
`bson.errors.InvalidId` (which would have surfaced as a 500 on
|
|
156
|
+
any future caller passing raw external input).
|
|
157
|
+
- **Per-IP rate-limit map covers `/login/mfa-confirm` and
|
|
158
|
+
`/oauth/exchange`.** Two new config fields:
|
|
159
|
+
`login_mfa_confirm_rate_limit`, `oauth_exchange_rate_limit`.
|
|
160
|
+
The per-code attempt counter on `mfa_codes` defends each
|
|
161
|
+
individual code; this adds the per-IP layer against distributed
|
|
162
|
+
guessing across many source IPs.
|
|
163
|
+
- **OAuth callback `error` query parameter sanitized before
|
|
164
|
+
logging.** A compromised or malicious OAuth provider could
|
|
165
|
+
previously inject newlines / ANSI escapes into the log stream
|
|
166
|
+
via the `error=...` redirect. The callback now strips control
|
|
167
|
+
characters and caps length at 200 before logging.
|
|
168
|
+
- **`oauth_states.mode` validated at the MongoDB storage layer.**
|
|
169
|
+
A `$jsonSchema` validator on the collection enforces
|
|
170
|
+
`mode IN ('signin', 'link')`, matching the SQL backend's
|
|
171
|
+
existing `CheckConstraint`. `OAuthState.model_validate()`
|
|
172
|
+
already enforced this at the app layer; this is defence-in-depth.
|
|
173
|
+
- **Migration `0002` downgrade refuses to roll back when OAuth-only
|
|
174
|
+
users exist.** The downgrade re-applies `NOT NULL` to
|
|
175
|
+
`users.hashed_password`; if any row has `NULL` (OAuth-only
|
|
176
|
+
signup), it now raises `RuntimeError` with a clear remediation
|
|
177
|
+
message instead of silently succeeding on SQLite (where
|
|
178
|
+
`batch_alter_table`'s CREATE-COPY-DROP-RENAME path skipped
|
|
179
|
+
NOT NULL enforcement).
|
|
180
|
+
- **PEP 740 sigstore attestations on the PyPI publish workflow.**
|
|
181
|
+
Each published wheel / sdist is now cryptographically bound to
|
|
182
|
+
the specific GitHub Actions run that produced it, so consumers
|
|
183
|
+
can verify the artefact came from this repo's CI.
|
|
184
|
+
- **`workflow_dispatch` removed from `publish.yml`.** Manual runs
|
|
185
|
+
previously uploaded artefacts to Actions storage with no
|
|
186
|
+
version validation, where they could be confused with a real
|
|
187
|
+
release build. Tag-push is the only supported trigger.
|
|
188
|
+
|
|
189
|
+
**Fixed.**
|
|
190
|
+
|
|
191
|
+
- **`regstack doctor --send-test-email` honours the new `from_name`
|
|
192
|
+
fall-back.** Before, the probe path passed `config.email.from_name`
|
|
193
|
+
(now `Optional[str]`) straight into `EmailMessage.from_name`
|
|
194
|
+
(typed `str`), producing a `None <addr>` From: header when
|
|
195
|
+
unset.
|
|
196
|
+
- **`install_schema()` survives a legacy unnamed unique-on-email
|
|
197
|
+
index.** A host that previously ran
|
|
198
|
+
`db.users.create_index([("email", 1)], unique=True)` from its
|
|
199
|
+
own pre-regstack auth code has a Mongo-auto-named `email_1`
|
|
200
|
+
index. `install_indexes` previously crashed on first boot with
|
|
201
|
+
`IndexOptionsConflict`. It now detects any unnamed/legacy
|
|
202
|
+
unique index over `{"email": 1}`, drops it, and proceeds.
|
|
203
|
+
Idempotent on a healthy DB.
|
|
204
|
+
- **`POST /verify` no longer 500s on the admin-promote-meets-user-
|
|
205
|
+
clicks-verify race.** The endpoint now catches
|
|
206
|
+
`UserAlreadyExistsError` from `users.create` and returns a
|
|
207
|
+
graceful 400 ("This email is already registered. Please sign
|
|
208
|
+
in.") instead of letting the unique-constraint violation bubble
|
|
209
|
+
up as a 500.
|
|
210
|
+
|
|
211
|
+
**Internal.**
|
|
212
|
+
|
|
213
|
+
- **GitHub Actions pinned ahead of Node 20 deprecation.**
|
|
214
|
+
`actions/checkout` v4→v6.0.2, `astral-sh/setup-uv` v3→v8.1.0,
|
|
215
|
+
`actions/upload-artifact` v4→v7.0.1, `actions/download-artifact`
|
|
216
|
+
v4→v8.0.1. All pins remain commit SHAs.
|
|
217
|
+
- **Daily scheduled security-review reports** land under
|
|
218
|
+
`docs/security-reports/` for 2026-05-15 through 2026-05-17.
|
|
219
|
+
The 2026-05-17 report is `[security-clean]`: all warnings from
|
|
220
|
+
the prior two days resolved in this release.
|
|
221
|
+
|
|
8
222
|
## 0.6.0 — 2026-05-14
|
|
9
223
|
|
|
10
224
|
**Breaking change for wizard users.** The GUI setup wizards
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: regstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/regstack
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/regstack
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pwdlib[argon2]>=0.2.1
|
|
|
28
28
|
Requires-Dist: pydantic-settings>=2.2
|
|
29
29
|
Requires-Dist: pydantic>=2.6
|
|
30
30
|
Requires-Dist: pyjwt>=2.12.1
|
|
31
|
-
Requires-Dist: python-multipart>=0.0.
|
|
31
|
+
Requires-Dist: python-multipart>=0.0.27
|
|
32
32
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: anyio>=4.3; extra == 'dev'
|
|
@@ -151,7 +151,7 @@ result everywhere is what regstack is for.
|
|
|
151
151
|
✔ Server-rendered HTML pages, theme with one CSS file
|
|
152
152
|
✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
|
|
153
153
|
✔ Argon2 password hashing, CSP-friendly templates
|
|
154
|
-
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview),
|
|
154
|
+
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), `regstack doctor` (config validator), and `regstack validate` (end-to-end probe of a deployed install)
|
|
155
155
|
✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
|
|
156
156
|
```
|
|
157
157
|
|
|
@@ -72,7 +72,7 @@ result everywhere is what regstack is for.
|
|
|
72
72
|
✔ Server-rendered HTML pages, theme with one CSS file
|
|
73
73
|
✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
|
|
74
74
|
✔ Argon2 password hashing, CSP-friendly templates
|
|
75
|
-
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview),
|
|
75
|
+
✔ Setup wizards (live in their own pywebview windows): `regstack init` (project bootstrap), `regstack oauth setup` (guided Google OAuth client config), `regstack theme design` (live theme designer with preview), `regstack doctor` (config validator), and `regstack validate` (end-to-end probe of a deployed install)
|
|
76
76
|
✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "regstack"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.0"
|
|
4
4
|
description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -31,9 +31,10 @@ dependencies = [
|
|
|
31
31
|
"jinja2>=3.1.6",
|
|
32
32
|
"click>=8.1",
|
|
33
33
|
"dnspython>=2.6",
|
|
34
|
-
# python-multipart>=0.0.
|
|
35
|
-
# multipart preamble)
|
|
36
|
-
|
|
34
|
+
# python-multipart>=0.0.27 picks up CVE-2026-40347 (DoS via oversized
|
|
35
|
+
# multipart preamble) and CVE-2026-42561 (DoS via unbounded part-header
|
|
36
|
+
# count/size — CVSS 7.5, network-exploitable).
|
|
37
|
+
"python-multipart>=0.0.27",
|
|
37
38
|
"email-validator>=2.1",
|
|
38
39
|
"aiosmtplib>=3.0",
|
|
39
40
|
# SQL stack — bundled by default because SQLite is the default backend.
|
|
@@ -118,6 +119,34 @@ packages = ["src/regstack"]
|
|
|
118
119
|
# Hatch's package autodiscovery already picks up template / static assets
|
|
119
120
|
# under src/regstack — no force-include needed (it would double-pack).
|
|
120
121
|
|
|
122
|
+
[tool.hatch.build.targets.sdist]
|
|
123
|
+
# Hatchling's sdist default includes every git-tracked file. Without this
|
|
124
|
+
# block the published source tarball ships internal planning docs,
|
|
125
|
+
# security-review prompts, the full test suite, and CI configuration to
|
|
126
|
+
# anyone who runs `pip download --no-binary`. The wheel target above is
|
|
127
|
+
# already tight (src/regstack only); this list is the matching sdist
|
|
128
|
+
# discipline. Flagged as W-2 in the 2026-05-15 / 2026-05-16 security
|
|
129
|
+
# reviews — CLAUDE.md in particular contains a developer home path.
|
|
130
|
+
exclude = [
|
|
131
|
+
".git",
|
|
132
|
+
".github/",
|
|
133
|
+
".python-version",
|
|
134
|
+
".readthedocs.yaml",
|
|
135
|
+
"CLAUDE.md",
|
|
136
|
+
"docs/",
|
|
137
|
+
"examples/",
|
|
138
|
+
"scripts/",
|
|
139
|
+
"tasks.py",
|
|
140
|
+
"tasks/",
|
|
141
|
+
"tests/",
|
|
142
|
+
"uv.lock",
|
|
143
|
+
]
|
|
144
|
+
# Note on `.git`: hatchling normally skips it because it's a directory, but
|
|
145
|
+
# when this repo is built from a git *worktree* (e.g. release prep on a
|
|
146
|
+
# branch), `.git` is a 1-line text file containing an absolute path to the
|
|
147
|
+
# primary repo's worktrees dir — which leaks the developer's home directory
|
|
148
|
+
# into PyPI. Excluding it explicitly is harmless for non-worktree builds.
|
|
149
|
+
|
|
121
150
|
[tool.pytest.ini_options]
|
|
122
151
|
minversion = "8.0"
|
|
123
152
|
asyncio_mode = "auto"
|
|
@@ -341,6 +341,60 @@ class RegStack:
|
|
|
341
341
|
)
|
|
342
342
|
return await self.users.create(user)
|
|
343
343
|
|
|
344
|
+
async def promote_pending(self, email: str) -> BaseUser:
|
|
345
|
+
"""Convert a pending registration directly into a verified user.
|
|
346
|
+
|
|
347
|
+
Bypasses the email-link round-trip. Useful when:
|
|
348
|
+
|
|
349
|
+
- a user lost their verification link and ``resend-verification``
|
|
350
|
+
isn't an option (admin-triggered onboarding, dev fixtures);
|
|
351
|
+
- a CLI batch operation seeds users from a known-good list;
|
|
352
|
+
- an admin is rescuing a stuck signup.
|
|
353
|
+
|
|
354
|
+
The pending row's ``hashed_password`` and ``full_name`` carry
|
|
355
|
+
over verbatim — the user logs in with the password they
|
|
356
|
+
originally registered with. The pending row is deleted on
|
|
357
|
+
success. Fires the ``user_verified`` hook so analytics /
|
|
358
|
+
downstream listeners see the same event the email-driven
|
|
359
|
+
``POST /verify`` produces.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
email: The email address whose pending registration should
|
|
363
|
+
be promoted.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
The newly persisted, active, verified
|
|
367
|
+
:class:`~regstack.models.user.BaseUser`.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
LookupError: If no pending registration exists for that
|
|
371
|
+
email (caller's job to surface as 404 / CLI error).
|
|
372
|
+
UserAlreadyExistsError: If a non-pending user with that
|
|
373
|
+
email already exists (caller's job to surface as 409).
|
|
374
|
+
"""
|
|
375
|
+
pending = await self.pending.find_by_email(email)
|
|
376
|
+
if pending is None:
|
|
377
|
+
raise LookupError(f"No pending registration for {email!r}.")
|
|
378
|
+
# Match ``POST /verify``'s contract: an expired pending row is
|
|
379
|
+
# not a valid promotion target. Mongo's TTL reap is eventual
|
|
380
|
+
# and the SQL backends only purge on ``purge_expired()``, so a
|
|
381
|
+
# row past its window can still appear here. Treat it as
|
|
382
|
+
# missing rather than silently promoting a stale invitation.
|
|
383
|
+
if pending.expires_at <= self.clock.now():
|
|
384
|
+
await self.pending.delete_by_email(pending.email)
|
|
385
|
+
raise LookupError(f"Pending registration for {email!r} has expired.")
|
|
386
|
+
user = BaseUser(
|
|
387
|
+
email=pending.email,
|
|
388
|
+
hashed_password=pending.hashed_password,
|
|
389
|
+
full_name=pending.full_name,
|
|
390
|
+
is_active=True,
|
|
391
|
+
is_verified=True,
|
|
392
|
+
)
|
|
393
|
+
user = await self.users.create(user)
|
|
394
|
+
await self.pending.delete_by_email(pending.email)
|
|
395
|
+
await self.hooks.fire("user_verified", user=user)
|
|
396
|
+
return user
|
|
397
|
+
|
|
344
398
|
# --- Extension surface ------------------------------------------------
|
|
345
399
|
|
|
346
400
|
def set_email_backend(self, service: EmailService) -> None:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Awaitable, Callable
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
5
|
|
|
6
6
|
from fastapi import Depends, HTTPException, Request, status
|
|
7
7
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
@@ -14,6 +14,10 @@ if TYPE_CHECKING:
|
|
|
14
14
|
from regstack.models.user import BaseUser
|
|
15
15
|
|
|
16
16
|
UserDependency = Callable[..., Awaitable["BaseUser"]]
|
|
17
|
+
# typing.Optional avoids a PEP-604 union-inside-string-forward-ref,
|
|
18
|
+
# which some mypy / pyright configurations refuse to resolve when
|
|
19
|
+
# BaseUser is only imported under TYPE_CHECKING.
|
|
20
|
+
OptionalUserDependency = Callable[..., Awaitable[Optional["BaseUser"]]]
|
|
17
21
|
|
|
18
22
|
_bearer = HTTPBearer(auto_error=False)
|
|
19
23
|
|
|
@@ -82,6 +86,37 @@ class AuthDependencies:
|
|
|
82
86
|
|
|
83
87
|
return _dep
|
|
84
88
|
|
|
89
|
+
def current_user_optional(self) -> OptionalUserDependency:
|
|
90
|
+
"""Return a FastAPI dependency that yields the user or ``None``.
|
|
91
|
+
|
|
92
|
+
For endpoints that render differently for authenticated vs
|
|
93
|
+
anonymous callers (think "show your cart icon if logged in").
|
|
94
|
+
Treats every form of auth failure — missing header, bad scheme,
|
|
95
|
+
expired token, revoked jti, deleted user, bulk-revoked session
|
|
96
|
+
— as anonymous and returns ``None`` rather than raising 401.
|
|
97
|
+
|
|
98
|
+
On success the user is stashed on
|
|
99
|
+
``request.state.regstack_user`` (same as :meth:`current_user`),
|
|
100
|
+
so downstream middleware sees the same shape for either path.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A callable suitable for ``Depends(...)``. Never raises an
|
|
104
|
+
``HTTPException`` — auth problems collapse to ``None``.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
async def _dep(
|
|
108
|
+
request: Request,
|
|
109
|
+
creds: HTTPAuthorizationCredentials | None = Depends(_bearer),
|
|
110
|
+
) -> BaseUser | None:
|
|
111
|
+
try:
|
|
112
|
+
user = await self._authenticate(creds)
|
|
113
|
+
except HTTPException:
|
|
114
|
+
return None
|
|
115
|
+
request.state.regstack_user = user
|
|
116
|
+
return user
|
|
117
|
+
|
|
118
|
+
return _dep
|
|
119
|
+
|
|
85
120
|
def current_admin(self) -> UserDependency:
|
|
86
121
|
"""Return a FastAPI dependency that yields a *superuser*.
|
|
87
122
|
|
|
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
|
|
|
43
43
|
# decoration walks the assembled APIRouter and matches by `route.path`.
|
|
44
44
|
ROUTE_LIMIT_MAP: dict[str, str] = {
|
|
45
45
|
"login_rate_limit": "/login",
|
|
46
|
+
"login_mfa_confirm_rate_limit": "/login/mfa-confirm",
|
|
46
47
|
"register_rate_limit": "/register",
|
|
47
48
|
"forgot_password_rate_limit": "/forgot-password",
|
|
48
49
|
"reset_password_rate_limit": "/reset-password",
|
|
@@ -52,6 +53,7 @@ ROUTE_LIMIT_MAP: dict[str, str] = {
|
|
|
52
53
|
"change_email_rate_limit": "/change-email",
|
|
53
54
|
"confirm_email_change_rate_limit": "/confirm-email-change",
|
|
54
55
|
"delete_account_rate_limit": "/account",
|
|
56
|
+
"oauth_exchange_rate_limit": "/oauth/exchange",
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from pymongo import ASCENDING, IndexModel
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from pymongo.asynchronous.database import AsyncDatabase
|
|
10
|
+
|
|
11
|
+
from regstack.backends.mongo.client import MongoDoc
|
|
12
|
+
from regstack.config.schema import RegStackConfig
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def install_indexes(db: AsyncDatabase[MongoDoc], config: RegStackConfig) -> None:
|
|
18
|
+
"""Create the indexes regstack relies on. Safe to call repeatedly."""
|
|
19
|
+
users = db[config.user_collection]
|
|
20
|
+
await _drop_conflicting_email_index(users)
|
|
21
|
+
await users.create_indexes(
|
|
22
|
+
[IndexModel([("email", ASCENDING)], unique=True, name="email_unique")]
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
blacklist = db[config.blacklist_collection]
|
|
26
|
+
# TTL on `exp` lets MongoDB reap revoked tokens when they would have
|
|
27
|
+
# expired anyway. expireAfterSeconds=0 means "delete when the date is
|
|
28
|
+
# in the past" — the value at `exp` is the deletion deadline.
|
|
29
|
+
await blacklist.create_indexes(
|
|
30
|
+
[
|
|
31
|
+
IndexModel([("jti", ASCENDING)], unique=True, name="jti_unique"),
|
|
32
|
+
IndexModel([("exp", ASCENDING)], expireAfterSeconds=0, name="exp_ttl"),
|
|
33
|
+
]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
pending = db[config.pending_collection]
|
|
37
|
+
await pending.create_indexes(
|
|
38
|
+
[
|
|
39
|
+
IndexModel([("email", ASCENDING)], unique=True, name="pending_email_unique"),
|
|
40
|
+
IndexModel([("token_hash", ASCENDING)], unique=True, name="pending_token_unique"),
|
|
41
|
+
IndexModel([("expires_at", ASCENDING)], expireAfterSeconds=0, name="pending_ttl"),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
attempts = db[config.login_attempt_collection]
|
|
46
|
+
# Sparse-ish TTL — rows survive `login_lockout_window_seconds` after
|
|
47
|
+
# `when`. The TTL value comes from config so tightening the lockout
|
|
48
|
+
# window also tightens cleanup.
|
|
49
|
+
await attempts.create_indexes(
|
|
50
|
+
[
|
|
51
|
+
IndexModel([("email", ASCENDING), ("when", ASCENDING)], name="email_when"),
|
|
52
|
+
IndexModel(
|
|
53
|
+
[("when", ASCENDING)],
|
|
54
|
+
expireAfterSeconds=config.login_lockout_window_seconds,
|
|
55
|
+
name="when_ttl",
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
mfa = db[config.mfa_code_collection]
|
|
61
|
+
await mfa.create_indexes(
|
|
62
|
+
[
|
|
63
|
+
IndexModel(
|
|
64
|
+
[("user_id", ASCENDING), ("kind", ASCENDING)],
|
|
65
|
+
unique=True,
|
|
66
|
+
name="user_kind_unique",
|
|
67
|
+
),
|
|
68
|
+
IndexModel([("expires_at", ASCENDING)], expireAfterSeconds=0, name="mfa_ttl"),
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
oauth_identities = db[config.oauth_identity_collection]
|
|
73
|
+
await oauth_identities.create_indexes(
|
|
74
|
+
[
|
|
75
|
+
IndexModel(
|
|
76
|
+
[("provider", ASCENDING), ("subject_id", ASCENDING)],
|
|
77
|
+
unique=True,
|
|
78
|
+
name="provider_subject_unique",
|
|
79
|
+
),
|
|
80
|
+
IndexModel(
|
|
81
|
+
[("user_id", ASCENDING), ("provider", ASCENDING)],
|
|
82
|
+
unique=True,
|
|
83
|
+
name="user_provider_unique",
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
oauth_states = db[config.oauth_state_collection]
|
|
89
|
+
await oauth_states.create_indexes(
|
|
90
|
+
[
|
|
91
|
+
IndexModel(
|
|
92
|
+
[("expires_at", ASCENDING)],
|
|
93
|
+
expireAfterSeconds=0,
|
|
94
|
+
name="oauth_state_ttl",
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
await _ensure_oauth_states_validator(db, config.oauth_state_collection)
|
|
99
|
+
|
|
100
|
+
log.info("regstack indexes installed on database %s", db.name)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def _drop_conflicting_email_index(users: Any) -> None:
|
|
104
|
+
"""Drop an unnamed unique index on ``email`` left over from a host's
|
|
105
|
+
pre-regstack auth code.
|
|
106
|
+
|
|
107
|
+
Mongo cannot rename an index in place. Hosts that previously ran
|
|
108
|
+
``db.users.create_index([("email", ASCENDING)], unique=True)`` from
|
|
109
|
+
their own code end up with the auto-generated name ``email_1`` on
|
|
110
|
+
the same key + uniqueness regstack wants under ``email_unique``.
|
|
111
|
+
A fresh ``install_indexes`` then raises ``IndexOptionsConflict``
|
|
112
|
+
on its first boot under regstack.
|
|
113
|
+
|
|
114
|
+
This helper detects ANY index over exactly ``{"email": 1}`` with
|
|
115
|
+
``unique=True`` that is not already named ``email_unique``, and
|
|
116
|
+
drops it so the canonical-name create on the next line succeeds.
|
|
117
|
+
We deliberately do not require the legacy name to be exactly
|
|
118
|
+
``email_1`` — any other rename (e.g. a host that named theirs
|
|
119
|
+
``users_email_uq``) would hit the same conflict.
|
|
120
|
+
|
|
121
|
+
Idempotent — re-running on a healthy database is a no-op because
|
|
122
|
+
the loop only matches indexes that aren't already the canonical
|
|
123
|
+
one. Safe even if the collection doesn't exist yet (the
|
|
124
|
+
``index_information`` call returns an empty dict).
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
existing = await users.index_information()
|
|
128
|
+
except Exception: # pragma: no cover — defensive; missing namespace
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
for name, info in existing.items():
|
|
132
|
+
if name in ("_id_", "email_unique"):
|
|
133
|
+
continue
|
|
134
|
+
key = info.get("key")
|
|
135
|
+
if key != [("email", 1)]:
|
|
136
|
+
continue
|
|
137
|
+
if not info.get("unique"):
|
|
138
|
+
continue
|
|
139
|
+
log.warning(
|
|
140
|
+
"Dropping legacy unique-on-email index %r on %s.users to "
|
|
141
|
+
"make room for email_unique (regstack canonical name).",
|
|
142
|
+
name,
|
|
143
|
+
users.database.name,
|
|
144
|
+
)
|
|
145
|
+
await users.drop_index(name)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _ensure_oauth_states_validator(db: AsyncDatabase[MongoDoc], collection_name: str) -> None:
|
|
149
|
+
"""Pin ``oauth_states.mode`` to ``signin`` / ``link`` at the DB level.
|
|
150
|
+
|
|
151
|
+
Mirrors the SQL backend's ``CheckConstraint("mode IN ('signin', 'link')")``.
|
|
152
|
+
``OAuthState.model_validate()`` already enforces this at the application
|
|
153
|
+
layer on every read, so this is defence-in-depth rather than a closed
|
|
154
|
+
exploit path — flagged as I-5 in the 2026-05-15 / 2026-05-16 security
|
|
155
|
+
reviews.
|
|
156
|
+
|
|
157
|
+
Uses ``validationLevel="moderate"`` so re-running ``install_indexes``
|
|
158
|
+
on a populated collection does not retroactively reject pre-existing
|
|
159
|
+
rows; the constraint applies to inserts and updates that touch
|
|
160
|
+
``mode``.
|
|
161
|
+
"""
|
|
162
|
+
from pymongo.errors import OperationFailure
|
|
163
|
+
|
|
164
|
+
validator = {
|
|
165
|
+
"$jsonSchema": {
|
|
166
|
+
"bsonType": "object",
|
|
167
|
+
"properties": {
|
|
168
|
+
"mode": {"enum": ["signin", "link"]},
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
try:
|
|
173
|
+
await db.command(
|
|
174
|
+
{
|
|
175
|
+
"collMod": collection_name,
|
|
176
|
+
"validator": validator,
|
|
177
|
+
"validationLevel": "moderate",
|
|
178
|
+
"validationAction": "error",
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
except OperationFailure as exc:
|
|
182
|
+
# collMod fails on a non-existent collection. Create it with the
|
|
183
|
+
# validator attached instead; either path leaves the collection
|
|
184
|
+
# with the schema in place.
|
|
185
|
+
if "NamespaceNotFound" not in str(exc) and exc.code != 26: # 26 = NamespaceNotFound
|
|
186
|
+
raise
|
|
187
|
+
await db.create_collection(
|
|
188
|
+
collection_name,
|
|
189
|
+
validator=validator,
|
|
190
|
+
validationLevel="moderate",
|
|
191
|
+
validationAction="error",
|
|
192
|
+
)
|