tempest-fastapi-sdk 0.48.0__tar.gz → 0.49.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.
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/CHANGELOG.md +19 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/PKG-INFO +11 -1
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/README.md +10 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/auth-flow.en.md +189 -21
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/auth-flow.md +189 -21
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/http.en.md +3 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/http.md +3 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/pyproject.toml +1 -1
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/__init__.py +1 -1
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/service.py +111 -3
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/auth/test_service.py +148 -1
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/uv.lock +1 -1
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/.github/workflows/ci.yml +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/.github/workflows/docs.yml +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/.github/workflows/release-pypi.yml +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/.gitignore +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/.python-version +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/CLAUDE.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/Makefile +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/architecture.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/architecture.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/changelog.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/changelog.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/contributing.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/contributing.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/index.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/index.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/installation.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/installation.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/index.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/index.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/api.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/api.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/business-rules.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/business-rules.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/domain.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/domain.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/flows.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/flows.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/index.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/learning/marketplace/index.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/migration.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/migration.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/admin.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/admin.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/br-helpers.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/br-helpers.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/cache.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/cache.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/cli.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/cli.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/database.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/database.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/deploy-safety.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/deploy-safety.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/downloads.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/downloads.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/email.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/email.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/http-client.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/http-client.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/idempotency.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/idempotency.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/index.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/index.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/logging.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/logging.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/metrics.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/metrics.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/mfa.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/mfa.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/multi-tenant.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/multi-tenant.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/observability.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/observability.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/outbox.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/outbox.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/queue-tasks.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/queue-tasks.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/realtime.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/realtime.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/security.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/security.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/sessions.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/sessions.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/storage.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/storage.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/testing.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/testing.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/uploads.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/uploads.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/utilities.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/utilities.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/webpush.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/webpush.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/websocket.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/recipes/websocket.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/reference.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/reference.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/roadmap.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/roadmap.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/tutorial.en.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/docs/tutorial.md +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/mkdocs.yml +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/scripts/extract_recipe.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/auth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/config.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/forms.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/session.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/site.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/static/admin.css +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/base.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/dashboard.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/detail.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/form.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/list.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/login.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/logs.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/admin/templates/mfa.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/cookies.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/dependencies/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/dependencies/auth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/handlers.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/body_size.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/cors.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/csrf.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/graceful.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/idempotency.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/middlewares/request_id.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/oauth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/routers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/routers/health.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/routers/logs.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/routers/metrics.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/routers/tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/server.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/static.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/tracing.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/api/webhooks.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/page_renderer.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/schemas.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/activation.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/activation_error.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/activation_success.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/password_reset.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/password_reset_error.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/password_reset_form.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/auth/templates/password_reset_success.html +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cache/decorator.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cache/redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/README.md.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/env.example.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/gitignore.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/main.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/pyproject.toml.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/app.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/auth.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/dependencies/resources.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/api/routers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/controllers/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/core/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/core/exceptions.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/core/settings.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/db/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/db/models/user.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/db/repositories/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/schemas/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/server.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/services/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/src/utils/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/tests/__init__.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/_templates/tests/test_smoke.py.tmpl +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/db.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/generate.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/lint.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/main.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/new.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/secrets.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/src_layers.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/cli/user.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/controllers/base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/core/context.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/core/enums.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/core/logging.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/_alembic_templates/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/_alembic_templates/env.py.template +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/connection.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/migrations.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/mixins.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/outbox.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/repository.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/slow_query.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/tenant.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/user_model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/user_recovery_code_model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/db/user_token_model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/conflict.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/forbidden.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/jwt.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/not_found.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/too_many_requests.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/unauthorized.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/upload.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/exceptions/validation.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/py.typed +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/queue/manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/link_headers.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/logs.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/pagination.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/schemas/response.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/services/base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/dependencies.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/middleware.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/schemas.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/service.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sessions/store.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/settings/base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/settings/mixins.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/sse/event_stream.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/storage/minio_client.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/tasks/manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/tasks/scheduler.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/testing/database.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/client_ip.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/datetime.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/dict.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/download.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/email.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/http_client.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/jwt.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/log.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/metrics.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/password.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/regex.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/throttle.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/totp.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/utils/upload.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/webpush/dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/webpush/schemas.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/websockets/hub.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/websockets/router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tempest_fastapi_sdk/websockets/schemas.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_auth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_forms.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_logs_nav.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_site.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/admin/test_user_model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_body_size.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_cookies.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_cors.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_csrf.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_dependencies_auth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_graceful.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_handlers.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_health_router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_idempotency.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_jwt_dependency.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_logs_router.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_oauth.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_prometheus.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_rate_limit.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_request_id_middleware.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_role_dependency.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_server.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_static.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_tool_spec.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_tracing.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_webhooks.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/api/test_webhooks_rsa.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/auth/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/auth/test_mfa.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cache/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cache/test_decorator.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cache/test_redis_manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_db.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_db_seed.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_docker_compose.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_generate.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_main.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_secrets.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/cli/test_user.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/conftest.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/controllers/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/controllers/test_base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/core/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/core/test_context.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/core/test_enums.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/core/test_logging.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_alembic_hooks.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_bulk_ops.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_connection.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_migrations.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_model.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_outbox.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_repository.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_safe_upgrade.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_slow_query.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/db/test_tenant.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/exceptions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/exceptions/test_exceptions.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/queue/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/queue/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/test_base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/test_cursor_pagination.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/test_link_headers.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/test_pagination.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/schemas/test_response.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/services/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/services/test_base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/sessions/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/sessions/test_sessions.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/settings/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/settings/test_base.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/settings/test_mixins.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/sse/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/sse/test_event_stream.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/storage/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/storage/test_minio_client.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/tasks/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/tasks/test_manager.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/tasks/test_scheduler.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/testing/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/testing/test_database.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_client_ip.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_datetime.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_dict.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_download.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_email.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_http_client.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_jwt.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_lazy_extras.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_log.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_metrics.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_opaque_token.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_password.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_regex.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_storage_backends.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_throttle.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/utils/test_upload.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/webpush/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/webpush/test_dispatcher.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/webpush/test_schemas.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/websockets/__init__.py +0 -0
- {tempest_fastapi_sdk-0.48.0 → tempest_fastapi_sdk-0.49.0}/tests/websockets/test_hub_and_router.py +0 -0
|
@@ -5,6 +5,25 @@ All notable changes to **tempest-fastapi-sdk** are listed below.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.49.0] — 2026-06-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`UserAuthService.current_user_dependency()` — built-in
|
|
13
|
+
authenticated-user dependency.** Projects no longer hand-write a
|
|
14
|
+
`load_user` callable plus a second `JWTUtils` to read the
|
|
15
|
+
`current_user` from a bearer token. The service now exposes
|
|
16
|
+
`get_user(subject, session)` (session-explicit), `load_user(subject)`
|
|
17
|
+
(opens its own session from the `db=` handle), and
|
|
18
|
+
`current_user_dependency(*, soft=False)` which wraps
|
|
19
|
+
`make_jwt_user_dependency` with the service's **own** `JWTUtils` and
|
|
20
|
+
`load_user` — so the token is verified with the same secret it was
|
|
21
|
+
signed with, eliminating the divergent-secret footgun. Wiring
|
|
22
|
+
collapses to `get_current_user = auth_service.current_user_dependency()`
|
|
23
|
+
/ `auth_service.current_user_dependency(soft=True)`. Requires the
|
|
24
|
+
service to be built with `db=` (already an accepted constructor arg).
|
|
25
|
+
The auth-flow and HTTP recipes now teach this path.
|
|
26
|
+
|
|
8
27
|
## [0.48.0] — 2026-06-11
|
|
9
28
|
|
|
10
29
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tempest-fastapi-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.49.0
|
|
4
4
|
Summary: Shared FastAPI building blocks: base schemas, ORM model, async repository, exceptions, pagination and settings — the conventions used across Tempest projects.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mauriciobenjamin700/tempest-fastapi-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/mauriciobenjamin700/tempest-fastapi-sdk
|
|
@@ -1156,6 +1156,16 @@ async def me(current: UserModel = Depends(get_current_user)) -> UserResponseSche
|
|
|
1156
1156
|
return UserResponseSchema.model_validate(current)
|
|
1157
1157
|
```
|
|
1158
1158
|
|
|
1159
|
+
> **Using the bundled flow?** If you wire auth with `UserAuthService`
|
|
1160
|
+
> (built with `db=`), skip the hand-written `load_user` and the extra
|
|
1161
|
+
> `JWTUtils` — the service builds the dependency for you and verifies
|
|
1162
|
+
> the token with the same secret it signs with:
|
|
1163
|
+
>
|
|
1164
|
+
> ```python
|
|
1165
|
+
> get_current_user = auth_service.current_user_dependency()
|
|
1166
|
+
> get_current_user_or_none = auth_service.current_user_dependency(soft=True)
|
|
1167
|
+
> ```
|
|
1168
|
+
|
|
1159
1169
|
#### Soft auth (optional user)
|
|
1160
1170
|
|
|
1161
1171
|
`get_current_user_or_none` above already uses `soft=True` — it returns `None` instead of raising on a missing or invalid token, so endpoints can work both authenticated and anonymous:
|
|
@@ -1054,6 +1054,16 @@ async def me(current: UserModel = Depends(get_current_user)) -> UserResponseSche
|
|
|
1054
1054
|
return UserResponseSchema.model_validate(current)
|
|
1055
1055
|
```
|
|
1056
1056
|
|
|
1057
|
+
> **Using the bundled flow?** If you wire auth with `UserAuthService`
|
|
1058
|
+
> (built with `db=`), skip the hand-written `load_user` and the extra
|
|
1059
|
+
> `JWTUtils` — the service builds the dependency for you and verifies
|
|
1060
|
+
> the token with the same secret it signs with:
|
|
1061
|
+
>
|
|
1062
|
+
> ```python
|
|
1063
|
+
> get_current_user = auth_service.current_user_dependency()
|
|
1064
|
+
> get_current_user_or_none = auth_service.current_user_dependency(soft=True)
|
|
1065
|
+
> ```
|
|
1066
|
+
|
|
1057
1067
|
#### Soft auth (optional user)
|
|
1058
1068
|
|
|
1059
1069
|
`get_current_user_or_none` above already uses `soft=True` — it returns `None` instead of raising on a missing or invalid token, so endpoints can work both authenticated and anonymous:
|
|
@@ -7,7 +7,7 @@ Since v0.31.0 the SDK ships the full local-account lifecycle — email + passwor
|
|
|
7
7
|
1. **[Minimum setup](#minimum-setup)** — extras install + wiring four objects (`AsyncDatabaseManager`, `EmailUtils`, `UserAuthService`, `make_auth_router`).
|
|
8
8
|
2. **[Concrete UserTokenModel](#concrete-usertokenmodel)** — `BaseUserTokenModel` is abstract, your project owns the concrete table.
|
|
9
9
|
3. **[Endpoints](#endpoints)** — table of the 5 endpoints + payload + behavior.
|
|
10
|
-
4. **[Settings
|
|
10
|
+
4. **[Settings — environment variables](#settings-environment-variables)** — env vars in **six groups** (JWT, password policy, email flow, TTL, URLs/templates, backend pages) — each in a typed table, not one blob.
|
|
11
11
|
5. **[Email anatomy: how link, template and URL fit together](#email-anatomy)** — disambiguates the three concepts that confuse readers the most.
|
|
12
12
|
6. **[Five operating modes](#five-operating-modes)** — production, dev with local SMTP (Mailhog / smtp4dev), dev without SMTP, CI without activation, and **backend-only** (links and pages served directly by the backend).
|
|
13
13
|
7. **[Mailhog vs smtp4dev — which to pick for local dev](#mailhog-vs-smtp4dev)** — comparison + copy-paste docker-compose snippets.
|
|
@@ -53,6 +53,7 @@ emails = EmailUtils(
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
auth_service = UserAuthService(
|
|
56
|
+
db=db, # required for current_user_dependency (see final section)
|
|
56
57
|
user_model=UserModel,
|
|
57
58
|
token_model=UserTokenModel,
|
|
58
59
|
auth_settings=settings, # mixes AuthSettings (see section 4)
|
|
@@ -131,9 +132,9 @@ uv run tempest db upgrade
|
|
|
131
132
|
|
|
132
133
|
---
|
|
133
134
|
|
|
134
|
-
## Settings
|
|
135
|
+
## Settings — environment variables
|
|
135
136
|
|
|
136
|
-
Mix
|
|
137
|
+
Every knob in the flow comes from settings mixins. Mix them into your `Settings` class:
|
|
137
138
|
|
|
138
139
|
```python
|
|
139
140
|
# src/core/settings.py
|
|
@@ -158,30 +159,99 @@ class Settings(
|
|
|
158
159
|
pass
|
|
159
160
|
|
|
160
161
|
|
|
161
|
-
settings = Settings()
|
|
162
|
+
settings: Settings = Settings()
|
|
162
163
|
```
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
!!! info "The attribute name **is** the env var name"
|
|
166
|
+
Every attribute in the tables below is read from an environment variable of the **same name**, case-sensitive, **no prefix**. `AUTH_PASSWORD_MIN_LENGTH` in `.env` → `settings.AUTH_PASSWORD_MIN_LENGTH`. They all have defaults — you only set what you want to change.
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
# .env — email flow
|
|
168
|
-
AUTH_AUTO_ACTIVATE=false # true = skip activation, return JWT immediately
|
|
169
|
-
AUTH_RETURN_TOKEN_IN_RESPONSE=false # true = link in body instead of email
|
|
170
|
-
AUTH_PASSWORD_MIN_LENGTH=12 # fully configurable (1+, e.g. 4); single source of truth
|
|
171
|
-
AUTH_PASSWORD_REQUIRE_COMPLEXITY=false # true = require lowercase + uppercase + digit + special AND force a floor of 8
|
|
168
|
+
The variables split across **two mixins** and **six concern groups**. They're separated on purpose: a password is not the same thing as an email, and authentication (JWT) is not the same thing as account activation.
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
AUTH_ACTIVATION_TTL_SECONDS=604800 # 7 days
|
|
175
|
-
AUTH_PASSWORD_RESET_TTL_SECONDS=3600 # 1 hour
|
|
170
|
+
### Group 1 — Authentication / JWT (`JWTSettings`)
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
AUTH_ACTIVATION_URL_TEMPLATE=https://app.example.com/activate?token={token}
|
|
179
|
-
AUTH_PASSWORD_RESET_URL_TEMPLATE=https://app.example.com/reset?token={token}
|
|
172
|
+
Controls the signing and lifetime of the tokens login returns. **It's the same `JWT_SECRET` the `get_current_user` dependency uses to verify** (see [Getting the `current_user`](#getting-the-current_user-from-the-request)).
|
|
180
173
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
| Env var | Type | Default | What it does |
|
|
175
|
+
|---------|------|---------|--------------|
|
|
176
|
+
| `JWT_SECRET` | `str` (≥32 bytes) | `change-me-…-32` | HMAC secret that signs the JWT. **Must be overridden in production.** |
|
|
177
|
+
| `JWT_ALGORITHM` | `str` | `HS256` | JOSE algorithm. `HS256`/`HS512` (symmetric secret) or `RS256` (key pair). |
|
|
178
|
+
| `JWT_ACCESS_TTL_SECONDS` | `int` (≥1) | `3600` | **Access token** lifetime (1 h). Short by design — renew via refresh. |
|
|
179
|
+
| `JWT_REFRESH_TTL_SECONDS` | `int` (≥1) | `604800` | **Refresh token** lifetime (7 days). |
|
|
180
|
+
| `JWT_ISSUER` | `str \| None` | `None` | `iss` claim. `None` omits the claim. |
|
|
181
|
+
|
|
182
|
+
!!! danger "The default `JWT_SECRET` leaks tokens"
|
|
183
|
+
The default `change-me-change-me-change-me-32` exists only to boot locally. In production, **anyone** with the default can forge a valid JWT. Generate a strong secret (`openssl rand -base64 48`) and inject it via a secret manager — never commit it.
|
|
184
|
+
|
|
185
|
+
### Group 2 — Password policy (`AuthSettings`)
|
|
186
|
+
|
|
187
|
+
| Env var | Type | Default | What it does |
|
|
188
|
+
|---------|------|---------|--------------|
|
|
189
|
+
| `AUTH_PASSWORD_MIN_LENGTH` | `int` (≥1) | `12` | Minimum length accepted on signup **and** reset. |
|
|
190
|
+
| `AUTH_PASSWORD_REQUIRE_COMPLEXITY` | `bool` | `false` | `true` = require 1 lowercase + 1 uppercase + 1 digit + 1 special character. |
|
|
191
|
+
|
|
192
|
+
These two interact — **this is where it usually gets confusing**. The exact rule:
|
|
193
|
+
|
|
194
|
+
- **`complexity=false` (default):** only length matters. Any password with `≥ AUTH_PASSWORD_MIN_LENGTH` characters passes, with no composition requirement.
|
|
195
|
+
- **`complexity=true`:** on top of the 4 character classes, the **effective** length floor becomes `max(AUTH_PASSWORD_MIN_LENGTH, 8)`. That is, an `AUTH_PASSWORD_MIN_LENGTH` below 8 is **ignored** while complexity is on.
|
|
196
|
+
|
|
197
|
+
Decision table:
|
|
198
|
+
|
|
199
|
+
| `MIN_LENGTH` | `REQUIRE_COMPLEXITY` | Password accepted when |
|
|
200
|
+
|--------------|----------------------|------------------------|
|
|
201
|
+
| `12` | `false` | `≥ 12` chars, any composition |
|
|
202
|
+
| `4` | `false` | `≥ 4` chars, any composition (low floor, dev-only) |
|
|
203
|
+
| `4` | `true` | `≥ 8` chars (floor 8 **overrides** the 4) **+** the 4 classes |
|
|
204
|
+
| `16` | `true` | `≥ 16` chars **+** the 4 classes |
|
|
205
|
+
|
|
206
|
+
!!! warning "The floor is the single source of truth"
|
|
207
|
+
The request schemas (`SignupSchema`, `PasswordResetConfirmSchema`) impose **no** length bound of their own — they delegate to these two vars. Lowering `AUTH_PASSWORD_MIN_LENGTH` to `4` genuinely relaxes validation on the route too. There is no hidden second limit in the schema "protecting" you.
|
|
208
|
+
|
|
209
|
+
### Group 3 — Email flow control (`AuthSettings`)
|
|
210
|
+
|
|
211
|
+
Decide **whether** and **how** the link reaches the user. They map directly to the [five operating modes](#five-operating-modes).
|
|
212
|
+
|
|
213
|
+
| Env var | Type | Default | What it does |
|
|
214
|
+
|---------|------|---------|--------------|
|
|
215
|
+
| `AUTH_AUTO_ACTIVATE` | `bool` | `false` | `true` = user is born active, skips activation, signup returns the JWT pair directly (Mode D). **Never in production.** |
|
|
216
|
+
| `AUTH_RETURN_TOKEN_IN_RESPONSE` | `bool` | `false` | `true` = activation/reset link goes in the JSON body instead of the email (Mode C). |
|
|
217
|
+
|
|
218
|
+
### Group 4 — Account token TTL (`AuthSettings`)
|
|
219
|
+
|
|
220
|
+
Lifetime of the **single-use** tokens (activation / reset) — distinct from the Group 1 JWTs.
|
|
221
|
+
|
|
222
|
+
| Env var | Type | Default | What it does |
|
|
223
|
+
|---------|------|---------|--------------|
|
|
224
|
+
| `AUTH_ACTIVATION_TTL_SECONDS` | `int` (≥60) | `604800` | Activation token lifetime (7 days). |
|
|
225
|
+
| `AUTH_PASSWORD_RESET_TTL_SECONDS` | `int` (≥60) | `3600` | Reset token lifetime (1 h). Shorter is safer. |
|
|
226
|
+
|
|
227
|
+
### Group 5 — Email URLs and templates (`AuthSettings`)
|
|
228
|
+
|
|
229
|
+
| Env var | Type | Default | What it does |
|
|
230
|
+
|---------|------|---------|--------------|
|
|
231
|
+
| `AUTH_ACTIVATION_URL_TEMPLATE` | `str` | `http://localhost:3000/activate?token={token}` | URL that goes in the email; `{token}` is substituted. **Points at the frontend** (except in Mode E). |
|
|
232
|
+
| `AUTH_PASSWORD_RESET_URL_TEMPLATE` | `str` | `http://localhost:3000/reset-password?token={token}` | Same, for reset. |
|
|
233
|
+
| `AUTH_ACTIVATION_TEMPLATE` | `str` | `activation.html` | Jinja2 filename of the activation **email HTML**, resolved against `EmailUtils.template_dir`. |
|
|
234
|
+
| `AUTH_PASSWORD_RESET_TEMPLATE` | `str` | `password_reset.html` | Same, for reset. |
|
|
235
|
+
|
|
236
|
+
!!! warning "URL template ≠ Jinja2 template"
|
|
237
|
+
`*_URL_TEMPLATE` is a `.format()` string with `{token}` — it's the **link**. `*_TEMPLATE` is the name of an `.html` file — it's the **email that wraps the link**. Confusing the two is the #1 mistake. Full detail in [Email anatomy](#email-anatomy).
|
|
238
|
+
|
|
239
|
+
### Group 6 — Backend-rendered pages (Mode E, `AuthSettings`)
|
|
240
|
+
|
|
241
|
+
Only relevant when `AUTH_BACKEND_LINKS=true`. See [Mode E](#five-operating-modes) for the full flow.
|
|
242
|
+
|
|
243
|
+
| Env var | Type | Default | What it does |
|
|
244
|
+
|---------|------|---------|--------------|
|
|
245
|
+
| `AUTH_BACKEND_LINKS` | `bool` | `false` | `true` = mounts 3 extra HTML endpoints; the email link points at the **backend**, not the frontend. |
|
|
246
|
+
| `AUTH_LOGIN_URL` | `str \| None` | `None` | Login URL on the "go to login" button of success pages. `None` hides the button. |
|
|
247
|
+
| `AUTH_ACTIVATION_SUCCESS_TEMPLATE` | `str` | `activation_success.html` | Activation OK HTML page. |
|
|
248
|
+
| `AUTH_ACTIVATION_ERROR_TEMPLATE` | `str` | `activation_error.html` | Activation error HTML page. |
|
|
249
|
+
| `AUTH_PASSWORD_RESET_FORM_TEMPLATE` | `str` | `password_reset_form.html` | New-password HTML form. |
|
|
250
|
+
| `AUTH_PASSWORD_RESET_SUCCESS_TEMPLATE` | `str` | `password_reset_success.html` | Reset OK HTML page. |
|
|
251
|
+
| `AUTH_PASSWORD_RESET_ERROR_TEMPLATE` | `str` | `password_reset_error.html` | Reset error HTML page. |
|
|
252
|
+
|
|
253
|
+
!!! note "MFA / TOTP has its own vars"
|
|
254
|
+
When `AUTH_MFA_ENABLED=true`, `AuthSettings` also exposes `AUTH_MFA_ISSUER`, `AUTH_MFA_RECOVERY_CODES_COUNT`, `AUTH_MFA_TOKEN_TTL_SECONDS` and `AUTH_MFA_VERIFY_WINDOW`. They're out of scope for this recipe (signup/activate/login/reset) — covered in the MFA recipe.
|
|
185
255
|
|
|
186
256
|
---
|
|
187
257
|
|
|
@@ -515,6 +585,104 @@ emails/ # ← template_dir="emails"
|
|
|
515
585
|
|
|
516
586
|
---
|
|
517
587
|
|
|
588
|
+
## Getting the `current_user` from the request
|
|
589
|
+
|
|
590
|
+
`make_auth_router` **issues** the JWT pair (login/activate return `access_token` + `refresh_token`). But what next? When the frontend sends `Authorization: Bearer <access_token>` to **your own** routes, you need a dependency that decodes the token and resolves the user.
|
|
591
|
+
|
|
592
|
+
Since v0.49.0, `UserAuthService` builds that dependency for you — `current_user_dependency()`. It:
|
|
593
|
+
|
|
594
|
+
1. Reads `Authorization: Bearer <jwt>` via `HTTPBearer`.
|
|
595
|
+
2. Decodes and verifies the JWT with **the same `JWTUtils` the service signs with** — no second secret to keep in sync.
|
|
596
|
+
3. Pulls the `sub` (user id) from the payload, opens a session from `db=`, and returns the persisted `UserModel`.
|
|
597
|
+
|
|
598
|
+
### 1. Declare the dependency once
|
|
599
|
+
|
|
600
|
+
The service already has `user_model`, `JWTUtils` and the session — so you don't write `load_user` by hand. Group both variants in `src/api/dependencies/auth.py`:
|
|
601
|
+
|
|
602
|
+
```python
|
|
603
|
+
# src/api/dependencies/auth.py
|
|
604
|
+
from src.api.app import auth_service
|
|
605
|
+
|
|
606
|
+
get_current_user = auth_service.current_user_dependency()
|
|
607
|
+
get_current_user_or_none = auth_service.current_user_dependency(soft=True)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
!!! info "Requires `db=` on `UserAuthService`"
|
|
611
|
+
`current_user_dependency` resolves the user by opening its own session, so the service must have been created with `db=` (the `AsyncDatabaseManager` from [Minimum setup](#minimum-setup)). Because it reuses the internal `self.jwt`, the token is verified with the **same** secret that signed it — the divergent-`JWT_SECRET` footgun is gone.
|
|
612
|
+
|
|
613
|
+
??? note "No `UserAuthService`? Build the dependency by hand"
|
|
614
|
+
If your service doesn't use the bundled flow, the `make_jwt_user_dependency` primitive accepts any `JWTUtils` + a one-argument async `user_loader`:
|
|
615
|
+
|
|
616
|
+
```python
|
|
617
|
+
from uuid import UUID
|
|
618
|
+
|
|
619
|
+
from tempest_fastapi_sdk import JWTUtils, make_jwt_user_dependency
|
|
620
|
+
|
|
621
|
+
from src.api.app import db
|
|
622
|
+
from src.core.settings import settings
|
|
623
|
+
from src.db.models import UserModel
|
|
624
|
+
from src.db.repositories import UserRepository
|
|
625
|
+
|
|
626
|
+
tokens: JWTUtils = JWTUtils(
|
|
627
|
+
secret=settings.JWT_SECRET,
|
|
628
|
+
algorithm=settings.JWT_ALGORITHM,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
async def load_user(subject: str) -> UserModel:
|
|
633
|
+
"""Resolve the JWT subject (a UUID string) to the persisted user."""
|
|
634
|
+
async with db.get_session_context() as session:
|
|
635
|
+
repo: UserRepository = UserRepository(session)
|
|
636
|
+
return await repo.get_by_id(UUID(subject))
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
get_current_user = make_jwt_user_dependency(tokens, load_user)
|
|
640
|
+
get_current_user_or_none = make_jwt_user_dependency(tokens, load_user, soft=True)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Heads up: here `tokens` **must** use the same `JWT_SECRET` / `JWT_ALGORITHM` as login, otherwise every valid token is rejected.
|
|
644
|
+
|
|
645
|
+
### 2. Inject it on the route with `Depends`
|
|
646
|
+
|
|
647
|
+
```python
|
|
648
|
+
# src/api/routers/users.py
|
|
649
|
+
from fastapi import APIRouter, Depends
|
|
650
|
+
|
|
651
|
+
from src.api.dependencies.auth import get_current_user
|
|
652
|
+
from src.db.models import UserModel
|
|
653
|
+
from src.schemas import UserResponseSchema
|
|
654
|
+
|
|
655
|
+
router: APIRouter = APIRouter(prefix="/users", tags=["users"])
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@router.get("/me")
|
|
659
|
+
async def me(current: UserModel = Depends(get_current_user)) -> UserResponseSchema:
|
|
660
|
+
"""Return the user who owns the request's bearer token."""
|
|
661
|
+
return UserResponseSchema.model_validate(current)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`current` **is** the `UserModel` the service resolved — typed, persisted, ready to use. Missing or invalid token → `401 UnauthorizedException` before the route body runs.
|
|
665
|
+
|
|
666
|
+
### 3. Optional auth — `soft=True`
|
|
667
|
+
|
|
668
|
+
For routes that work both authenticated **and** anonymous (e.g. a public feed that personalizes when logged in), use the `soft` variant — it returns `None` instead of raising:
|
|
669
|
+
|
|
670
|
+
```python
|
|
671
|
+
@router.get("/feed")
|
|
672
|
+
async def feed(
|
|
673
|
+
current: UserModel | None = Depends(get_current_user_or_none),
|
|
674
|
+
) -> list[PostResponseSchema]:
|
|
675
|
+
"""Public feed; personalizes the ranking when a user is logged in."""
|
|
676
|
+
if current is None:
|
|
677
|
+
return await feed_service.public()
|
|
678
|
+
return await feed_service.personalized(current.id)
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
!!! tip "Role and permission are the next step"
|
|
682
|
+
When the route needs a **role** (`admin`) or **permission** (`users:write`) and not just "logged in", swap for `make_role_dependency` / `make_permission_dependency`. See the [HTTP recipe »](http.en.md) — same `JWTUtils`, same `Depends` pattern.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
518
686
|
## Next steps
|
|
519
687
|
|
|
520
688
|
- **[Idempotency »](idempotency.en.md)** — protect `POST /auth/signup` from retries that would duplicate the row.
|
|
@@ -7,7 +7,7 @@ Desde v0.31.0 o SDK fornece o ciclo completo de conta local — signup com email
|
|
|
7
7
|
1. **[Setup mínimo](#setup-minimo)** — instalação dos extras + wiring de quatro objetos (`AsyncDatabaseManager`, `EmailUtils`, `UserAuthService`, `make_auth_router`).
|
|
8
8
|
2. **[UserTokenModel concreto](#usertokenmodel-concreto)** — `BaseUserTokenModel` é abstrato, projeto cria a tabela final.
|
|
9
9
|
3. **[Endpoints](#endpoints)** — tabela dos 5 endpoints + payload + comportamento.
|
|
10
|
-
4. **[Settings
|
|
10
|
+
4. **[Settings — variáveis de ambiente](#settings-variaveis-de-ambiente)** — env vars em **seis grupos** (JWT, política de senha, fluxo de e-mail, TTL, URLs/templates, páginas backend) — cada uma em tabela tipada, não num blob.
|
|
11
11
|
5. **[Anatomia de um e-mail: como link, template e URL se encaixam](#anatomia-de-um-e-mail)** — desambigua os três conceitos que mais confundem.
|
|
12
12
|
6. **[Cinco modos de operação](#cinco-modos-de-operacao)** — produção, dev com SMTP local (Mailhog / smtp4dev), dev sem SMTP, CI sem ativação e **backend-only** (links e páginas servidas direto pelo backend).
|
|
13
13
|
7. **[Mailhog vs smtp4dev — qual escolher pra dev local](#mailhog-vs-smtp4dev)** — comparativo + receitas docker-compose copy-paste.
|
|
@@ -53,6 +53,7 @@ emails = EmailUtils(
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
auth_service = UserAuthService(
|
|
56
|
+
db=db, # necessário pra current_user_dependency (ver seção final)
|
|
56
57
|
user_model=UserModel,
|
|
57
58
|
token_model=UserTokenModel,
|
|
58
59
|
auth_settings=settings, # mistura AuthSettings (ver seção 4)
|
|
@@ -131,9 +132,9 @@ uv run tempest db upgrade
|
|
|
131
132
|
|
|
132
133
|
---
|
|
133
134
|
|
|
134
|
-
## Settings
|
|
135
|
+
## Settings — variáveis de ambiente
|
|
135
136
|
|
|
136
|
-
Mixe
|
|
137
|
+
Toda a config do flow vem de mixins de settings. Mixe-as na sua classe `Settings`:
|
|
137
138
|
|
|
138
139
|
```python
|
|
139
140
|
# src/core/settings.py
|
|
@@ -158,30 +159,99 @@ class Settings(
|
|
|
158
159
|
pass
|
|
159
160
|
|
|
160
161
|
|
|
161
|
-
settings = Settings()
|
|
162
|
+
settings: Settings = Settings()
|
|
162
163
|
```
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
!!! info "Nome do atributo **é** o nome da env var"
|
|
166
|
+
Cada atributo das tabelas abaixo é lido de uma variável de ambiente de **mesmo nome**, case-sensitive, **sem prefixo**. `AUTH_PASSWORD_MIN_LENGTH` no `.env` → `settings.AUTH_PASSWORD_MIN_LENGTH`. Todos têm default — você só seta o que quer mudar.
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
# .env — fluxo de e-mail
|
|
168
|
-
AUTH_AUTO_ACTIVATE=false # true = pula activation, devolve JWT direto
|
|
169
|
-
AUTH_RETURN_TOKEN_IN_RESPONSE=false # true = link no body em vez de e-mail
|
|
170
|
-
AUTH_PASSWORD_MIN_LENGTH=12 # totalmente configurável (1+, ex.: 4); fonte única da verdade
|
|
171
|
-
AUTH_PASSWORD_REQUIRE_COMPLEXITY=false # true = exige minúscula + maiúscula + dígito + especial E força piso de 8
|
|
168
|
+
As variáveis se dividem em **dois mixins** e **seis grupos de concern**. Elas estão separadas de propósito: senha não é a mesma coisa que e-mail, e autenticação (JWT) não é a mesma coisa que ativação de conta.
|
|
172
169
|
|
|
173
|
-
|
|
174
|
-
AUTH_ACTIVATION_TTL_SECONDS=604800 # 7 dias
|
|
175
|
-
AUTH_PASSWORD_RESET_TTL_SECONDS=3600 # 1 hora
|
|
170
|
+
### Grupo 1 — Autenticação / JWT (`JWTSettings`)
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
AUTH_ACTIVATION_URL_TEMPLATE=https://app.example.com/activate?token={token}
|
|
179
|
-
AUTH_PASSWORD_RESET_URL_TEMPLATE=https://app.example.com/reset?token={token}
|
|
172
|
+
Controla a assinatura e validade dos tokens que o login devolve. **É o mesmo `JWT_SECRET` que a dependency `get_current_user` usa pra validar** (veja [Pegando o `current_user`](#pegando-o-current_user-da-requisicao)).
|
|
180
173
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
| Env var | Tipo | Default | O que faz |
|
|
175
|
+
|---------|------|---------|-----------|
|
|
176
|
+
| `JWT_SECRET` | `str` (≥32 bytes) | `change-me-…-32` | Segredo HMAC que assina o JWT. **Obrigatório trocar em produção.** |
|
|
177
|
+
| `JWT_ALGORITHM` | `str` | `HS256` | Algoritmo JOSE. `HS256`/`HS512` (segredo simétrico) ou `RS256` (par de chaves). |
|
|
178
|
+
| `JWT_ACCESS_TTL_SECONDS` | `int` (≥1) | `3600` | Validade do **access token** (1 h). Curto por design — renove via refresh. |
|
|
179
|
+
| `JWT_REFRESH_TTL_SECONDS` | `int` (≥1) | `604800` | Validade do **refresh token** (7 dias). |
|
|
180
|
+
| `JWT_ISSUER` | `str \| None` | `None` | Claim `iss`. `None` omite o claim. |
|
|
181
|
+
|
|
182
|
+
!!! danger "`JWT_SECRET` default vaza tokens"
|
|
183
|
+
O default `change-me-change-me-change-me-32` existe só pra subir local. Em produção, **qualquer um** com o default consegue forjar um JWT válido. Gere um segredo forte (`openssl rand -base64 48`) e injete por secret manager — nunca commite.
|
|
184
|
+
|
|
185
|
+
### Grupo 2 — Política de senha (`AuthSettings`)
|
|
186
|
+
|
|
187
|
+
| Env var | Tipo | Default | O que faz |
|
|
188
|
+
|---------|------|---------|-----------|
|
|
189
|
+
| `AUTH_PASSWORD_MIN_LENGTH` | `int` (≥1) | `12` | Comprimento mínimo aceito no signup **e** no reset. |
|
|
190
|
+
| `AUTH_PASSWORD_REQUIRE_COMPLEXITY` | `bool` | `false` | `true` = exige 1 minúscula + 1 maiúscula + 1 dígito + 1 caractere especial. |
|
|
191
|
+
|
|
192
|
+
Os dois interagem — **é aqui que costuma confundir**. A regra exata:
|
|
193
|
+
|
|
194
|
+
- **`complexity=false` (default):** só o comprimento importa. Qualquer senha com `≥ AUTH_PASSWORD_MIN_LENGTH` caracteres passa, sem exigência de composição.
|
|
195
|
+
- **`complexity=true`:** além das 4 classes de caracteres, o piso de comprimento **efetivo** vira `max(AUTH_PASSWORD_MIN_LENGTH, 8)`. Ou seja, um `AUTH_PASSWORD_MIN_LENGTH` abaixo de 8 é **ignorado** enquanto a complexidade está ligada.
|
|
196
|
+
|
|
197
|
+
Tabela de decisão:
|
|
198
|
+
|
|
199
|
+
| `MIN_LENGTH` | `REQUIRE_COMPLEXITY` | Senha aceita quando |
|
|
200
|
+
|--------------|---------------------|---------------------|
|
|
201
|
+
| `12` | `false` | `≥ 12` chars, qualquer composição |
|
|
202
|
+
| `4` | `false` | `≥ 4` chars, qualquer composição (piso baixo, dev-only) |
|
|
203
|
+
| `4` | `true` | `≥ 8` chars (piso 8 **sobrescreve** o 4) **+** as 4 classes |
|
|
204
|
+
| `16` | `true` | `≥ 16` chars **+** as 4 classes |
|
|
205
|
+
|
|
206
|
+
!!! warning "O piso é fonte única da verdade"
|
|
207
|
+
Os schemas de request (`SignupSchema`, `PasswordResetConfirmSchema`) **não** impõem limite próprio de comprimento — eles delegam pra essas duas vars. Baixar `AUTH_PASSWORD_MIN_LENGTH` pra `4` realmente afrouxa a validação na rota também. Não há um segundo limite escondido no schema "te protegendo".
|
|
208
|
+
|
|
209
|
+
### Grupo 3 — Controle do fluxo de e-mail (`AuthSettings`)
|
|
210
|
+
|
|
211
|
+
Decidem **se** e **como** o link chega ao usuário. Mapeiam direto nos [cinco modos de operação](#cinco-modos-de-operacao).
|
|
212
|
+
|
|
213
|
+
| Env var | Tipo | Default | O que faz |
|
|
214
|
+
|---------|------|---------|-----------|
|
|
215
|
+
| `AUTH_AUTO_ACTIVATE` | `bool` | `false` | `true` = user nasce ativo, pula activation, signup devolve JWT pair direto (Modo D). **Nunca em produção.** |
|
|
216
|
+
| `AUTH_RETURN_TOKEN_IN_RESPONSE` | `bool` | `false` | `true` = link de ativação/reset vai no corpo JSON em vez do e-mail (Modo C). |
|
|
217
|
+
|
|
218
|
+
### Grupo 4 — TTL dos tokens de conta (`AuthSettings`)
|
|
219
|
+
|
|
220
|
+
Validade dos tokens **de uso único** (ativação / reset) — distintos dos JWT do grupo 1.
|
|
221
|
+
|
|
222
|
+
| Env var | Tipo | Default | O que faz |
|
|
223
|
+
|---------|------|---------|-----------|
|
|
224
|
+
| `AUTH_ACTIVATION_TTL_SECONDS` | `int` (≥60) | `604800` | Validade do token de ativação (7 dias). |
|
|
225
|
+
| `AUTH_PASSWORD_RESET_TTL_SECONDS` | `int` (≥60) | `3600` | Validade do token de reset (1 h). Curto é mais seguro. |
|
|
226
|
+
|
|
227
|
+
### Grupo 5 — URLs e templates de e-mail (`AuthSettings`)
|
|
228
|
+
|
|
229
|
+
| Env var | Tipo | Default | O que faz |
|
|
230
|
+
|---------|------|---------|-----------|
|
|
231
|
+
| `AUTH_ACTIVATION_URL_TEMPLATE` | `str` | `http://localhost:3000/activate?token={token}` | URL que vai no e-mail; `{token}` é substituído. **Aponta pro frontend** (exceto no Modo E). |
|
|
232
|
+
| `AUTH_PASSWORD_RESET_URL_TEMPLATE` | `str` | `http://localhost:3000/reset-password?token={token}` | Idem, para reset. |
|
|
233
|
+
| `AUTH_ACTIVATION_TEMPLATE` | `str` | `activation.html` | Nome do arquivo Jinja2 do **HTML do e-mail** de ativação, resolvido no `template_dir` do `EmailUtils`. |
|
|
234
|
+
| `AUTH_PASSWORD_RESET_TEMPLATE` | `str` | `password_reset.html` | Idem, para reset. |
|
|
235
|
+
|
|
236
|
+
!!! warning "URL template ≠ Jinja2 template"
|
|
237
|
+
`*_URL_TEMPLATE` é uma string `.format()` com `{token}` — é o **link**. `*_TEMPLATE` é o nome de um arquivo `.html` — é o **e-mail que embrulha o link**. Confundir os dois é o erro nº 1. Detalhe completo em [Anatomia de um e-mail](#anatomia-de-um-e-mail).
|
|
238
|
+
|
|
239
|
+
### Grupo 6 — Páginas renderizadas pelo backend (Modo E, `AuthSettings`)
|
|
240
|
+
|
|
241
|
+
Só relevantes quando `AUTH_BACKEND_LINKS=true`. Veja o [Modo E](#cinco-modos-de-operacao) para o fluxo completo.
|
|
242
|
+
|
|
243
|
+
| Env var | Tipo | Default | O que faz |
|
|
244
|
+
|---------|------|---------|-----------|
|
|
245
|
+
| `AUTH_BACKEND_LINKS` | `bool` | `false` | `true` = monta 3 endpoints HTML extras; o link do e-mail aponta pro **backend**, não pro frontend. |
|
|
246
|
+
| `AUTH_LOGIN_URL` | `str \| None` | `None` | URL de login no botão "Ir pro login" das páginas de sucesso. `None` esconde o botão. |
|
|
247
|
+
| `AUTH_ACTIVATION_SUCCESS_TEMPLATE` | `str` | `activation_success.html` | Página HTML de ativação OK. |
|
|
248
|
+
| `AUTH_ACTIVATION_ERROR_TEMPLATE` | `str` | `activation_error.html` | Página HTML de ativação com erro. |
|
|
249
|
+
| `AUTH_PASSWORD_RESET_FORM_TEMPLATE` | `str` | `password_reset_form.html` | Form HTML de nova senha. |
|
|
250
|
+
| `AUTH_PASSWORD_RESET_SUCCESS_TEMPLATE` | `str` | `password_reset_success.html` | Página HTML de reset OK. |
|
|
251
|
+
| `AUTH_PASSWORD_RESET_ERROR_TEMPLATE` | `str` | `password_reset_error.html` | Página HTML de reset com erro. |
|
|
252
|
+
|
|
253
|
+
!!! note "MFA / TOTP tem suas próprias vars"
|
|
254
|
+
Quando `AUTH_MFA_ENABLED=true`, o `AuthSettings` ainda expõe `AUTH_MFA_ISSUER`, `AUTH_MFA_RECOVERY_CODES_COUNT`, `AUTH_MFA_TOKEN_TTL_SECONDS` e `AUTH_MFA_VERIFY_WINDOW`. Ficam fora do escopo desta receita (signup/activate/login/reset) — são cobertos na receita de MFA.
|
|
185
255
|
|
|
186
256
|
---
|
|
187
257
|
|
|
@@ -515,6 +585,104 @@ emails/ # ← template_dir="emails"
|
|
|
515
585
|
|
|
516
586
|
---
|
|
517
587
|
|
|
588
|
+
## Pegando o `current_user` da requisição
|
|
589
|
+
|
|
590
|
+
`make_auth_router` **emite** o JWT pair (login/activate devolvem `access_token` + `refresh_token`). Mas e depois? Quando o frontend manda `Authorization: Bearer <access_token>` nas **suas próprias** rotas, você precisa de uma dependency que decodifica o token e resolve o usuário.
|
|
591
|
+
|
|
592
|
+
Desde a v0.49.0, o próprio `UserAuthService` constrói essa dependency — `current_user_dependency()`. Ela:
|
|
593
|
+
|
|
594
|
+
1. Lê `Authorization: Bearer <jwt>` via `HTTPBearer`.
|
|
595
|
+
2. Decodifica e verifica o JWT com **o mesmo `JWTUtils` que o service usou pra assinar** — sem segundo segredo pra manter sincronizado.
|
|
596
|
+
3. Pega o `sub` (id do usuário) do payload, abre uma sessão a partir do `db=` e devolve o `UserModel` persistido.
|
|
597
|
+
|
|
598
|
+
### 1. Declare a dependency uma vez
|
|
599
|
+
|
|
600
|
+
O service já tem `user_model`, `JWTUtils` e a sessão — então não precisa escrever `load_user` à mão. Junte as duas variantes no `src/api/dependencies/auth.py`:
|
|
601
|
+
|
|
602
|
+
```python
|
|
603
|
+
# src/api/dependencies/auth.py
|
|
604
|
+
from src.api.app import auth_service
|
|
605
|
+
|
|
606
|
+
get_current_user = auth_service.current_user_dependency()
|
|
607
|
+
get_current_user_or_none = auth_service.current_user_dependency(soft=True)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
!!! info "Requer `db=` no `UserAuthService`"
|
|
611
|
+
`current_user_dependency` resolve o usuário abrindo a própria sessão, então o service precisa ter sido criado com `db=` (o `AsyncDatabaseManager` do [Setup mínimo](#setup-minimo)). Como reusa o `self.jwt` interno, o token é validado com o **mesmo** segredo que assinou — o footgun de `JWT_SECRET` divergente some.
|
|
612
|
+
|
|
613
|
+
??? note "Sem `UserAuthService`? Monte a dependency na mão"
|
|
614
|
+
Se o seu serviço não usa o flow bundled, a primitiva `make_jwt_user_dependency` aceita qualquer `JWTUtils` + um `user_loader` async de um argumento:
|
|
615
|
+
|
|
616
|
+
```python
|
|
617
|
+
from uuid import UUID
|
|
618
|
+
|
|
619
|
+
from tempest_fastapi_sdk import JWTUtils, make_jwt_user_dependency
|
|
620
|
+
|
|
621
|
+
from src.api.app import db
|
|
622
|
+
from src.core.settings import settings
|
|
623
|
+
from src.db.models import UserModel
|
|
624
|
+
from src.db.repositories import UserRepository
|
|
625
|
+
|
|
626
|
+
tokens: JWTUtils = JWTUtils(
|
|
627
|
+
secret=settings.JWT_SECRET,
|
|
628
|
+
algorithm=settings.JWT_ALGORITHM,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
async def load_user(subject: str) -> UserModel:
|
|
633
|
+
"""Resolve o subject do JWT (uma string UUID) para o usuário persistido."""
|
|
634
|
+
async with db.get_session_context() as session:
|
|
635
|
+
repo: UserRepository = UserRepository(session)
|
|
636
|
+
return await repo.get_by_id(UUID(subject))
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
get_current_user = make_jwt_user_dependency(tokens, load_user)
|
|
640
|
+
get_current_user_or_none = make_jwt_user_dependency(tokens, load_user, soft=True)
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Atenção: aqui o `tokens` **precisa** usar o mesmo `JWT_SECRET` / `JWT_ALGORITHM` do login, senão todo token válido é rejeitado.
|
|
644
|
+
|
|
645
|
+
### 2. Injete na rota com `Depends`
|
|
646
|
+
|
|
647
|
+
```python
|
|
648
|
+
# src/api/routers/users.py
|
|
649
|
+
from fastapi import APIRouter, Depends
|
|
650
|
+
|
|
651
|
+
from src.api.dependencies.auth import get_current_user
|
|
652
|
+
from src.db.models import UserModel
|
|
653
|
+
from src.schemas import UserResponseSchema
|
|
654
|
+
|
|
655
|
+
router: APIRouter = APIRouter(prefix="/users", tags=["users"])
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
@router.get("/me")
|
|
659
|
+
async def me(current: UserModel = Depends(get_current_user)) -> UserResponseSchema:
|
|
660
|
+
"""Devolve o usuário dono do bearer token da requisição."""
|
|
661
|
+
return UserResponseSchema.model_validate(current)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
`current` **é** o `UserModel` que o service resolveu — tipado, persistido, pronto pra uso. Token ausente ou inválido → `401 UnauthorizedException` antes do corpo da rota rodar.
|
|
665
|
+
|
|
666
|
+
### 3. Auth opcional — `soft=True`
|
|
667
|
+
|
|
668
|
+
Para rotas que funcionam logado **e** anônimo (ex.: feed público que personaliza se houver login), use a variante `soft` — ela devolve `None` em vez de levantar:
|
|
669
|
+
|
|
670
|
+
```python
|
|
671
|
+
@router.get("/feed")
|
|
672
|
+
async def feed(
|
|
673
|
+
current: UserModel | None = Depends(get_current_user_or_none),
|
|
674
|
+
) -> list[PostResponseSchema]:
|
|
675
|
+
"""Feed público; personaliza o ranking quando há usuário logado."""
|
|
676
|
+
if current is None:
|
|
677
|
+
return await feed_service.public()
|
|
678
|
+
return await feed_service.personalized(current.id)
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
!!! tip "Role e permission são o próximo passo"
|
|
682
|
+
Quando a rota precisa de **papel** (`admin`) ou **permissão** (`users:write`) e não só "estar logado", troque por `make_role_dependency` / `make_permission_dependency`. Veja a [receita HTTP »](http.md) — mesma `JWTUtils`, mesmo padrão de `Depends`.
|
|
683
|
+
|
|
684
|
+
---
|
|
685
|
+
|
|
518
686
|
## Próximos passos
|
|
519
687
|
|
|
520
688
|
- **[Idempotência »](idempotency.md)** — proteja `POST /auth/signup` de retentativas que duplicariam linha.
|
|
@@ -114,6 +114,9 @@ Four dependency factories live in `tempest_fastapi_sdk.api.dependencies.auth`
|
|
|
114
114
|
| `make_jwt_user_dependency(tokens, user_loader, soft=False, subject_claim="sub")` | Decode the bearer JWT, await `user_loader(subject)`, return the loaded user. |
|
|
115
115
|
| `make_role_dependency(tokens, ["admin"], require_all=False, roles_claim="roles")` / `make_permission_dependency(tokens, ["users:write"], require_all=True, permissions_claim="permissions")` | Decode the bearer JWT and gate the route on roles / permissions. |
|
|
116
116
|
|
|
117
|
+
!!! tip "Using the bundled flow? Skip `load_user`"
|
|
118
|
+
If you wire auth with `UserAuthService` + `make_auth_router`, you don't write `load_user` or instantiate a `JWTUtils` here — call `auth_service.current_user_dependency()` (and `.current_user_dependency(soft=True)`), which reuses the service's internal `JWTUtils`. See the [auth recipe »](auth-flow.en.md#getting-the-current_user-from-the-request). The example below is the manual wiring, for when you're **not** using the service.
|
|
119
|
+
|
|
117
120
|
```python
|
|
118
121
|
# src/api/dependencies/auth.py
|
|
119
122
|
from uuid import UUID
|
|
@@ -114,6 +114,9 @@ Quatro factories de dependência vivem em `tempest_fastapi_sdk.api.dependencies.
|
|
|
114
114
|
| `make_jwt_user_dependency(tokens, user_loader, soft=False, subject_claim="sub")` | Decodifica o bearer JWT, aguarda `user_loader(subject)`, retorna o usuário carregado. |
|
|
115
115
|
| `make_role_dependency(tokens, ["admin"], require_all=False, roles_claim="roles")` / `make_permission_dependency(tokens, ["users:write"], require_all=True, permissions_claim="permissions")` | Decodifica o bearer JWT e controla a rota por roles / permissões. |
|
|
116
116
|
|
|
117
|
+
!!! tip "Usa o flow bundled? Pule o `load_user`"
|
|
118
|
+
Se você monta auth com `UserAuthService` + `make_auth_router`, não precisa escrever `load_user` nem instanciar um `JWTUtils` aqui — chame `auth_service.current_user_dependency()` (e `.current_user_dependency(soft=True)`), que reusa o `JWTUtils` interno do service. Veja a [receita de auth »](auth-flow.md#pegando-o-current_user-da-requisicao). O exemplo abaixo é a montagem manual, pra quando você **não** usa o service.
|
|
119
|
+
|
|
117
120
|
```python
|
|
118
121
|
# src/api/dependencies/auth.py
|
|
119
122
|
from uuid import UUID
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tempest-fastapi-sdk"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.49.0"
|
|
4
4
|
description = "Shared FastAPI building blocks: base schemas, ORM model, async repository, exceptions, pagination and settings — the conventions used across Tempest projects."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|