svc-infra 0.1.593__tar.gz → 0.1.595__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.
Potentially problematic release.
This version of svc-infra might be problematic. Click here for more details.
- {svc_infra-0.1.593 → svc_infra-0.1.595}/PKG-INFO +1 -1
- {svc_infra-0.1.593 → svc_infra-0.1.595}/pyproject.toml +5 -1
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/README.md +26 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/provider/aiydan.py +28 -2
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/service.py +113 -20
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/apf_payments/router.py +67 -4
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/add.py +10 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/gaurd.py +67 -5
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/routers/oauth_router.py +79 -34
- svc_infra-0.1.595/src/svc_infra/api/fastapi/auth/routers/session_router.py +63 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/settings.py +2 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/users.py +13 -1
- svc_infra-0.1.595/src/svc_infra/api/fastapi/dependencies/ratelimit.py +66 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/ratelimit.py +26 -11
- svc_infra-0.1.595/src/svc_infra/api/fastapi/middleware/ratelimit_store.py +78 -0
- svc_infra-0.1.595/src/svc_infra/api/fastapi/middleware/request_size_limit.py +36 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/setup.py +2 -1
- svc_infra-0.1.595/src/svc_infra/obs/metrics/__init__.py +53 -0
- svc_infra-0.1.595/src/svc_infra/obs/metrics.py +52 -0
- svc_infra-0.1.595/src/svc_infra/security/audit.py +130 -0
- svc_infra-0.1.595/src/svc_infra/security/audit_service.py +73 -0
- svc_infra-0.1.595/src/svc_infra/security/headers.py +39 -0
- svc_infra-0.1.595/src/svc_infra/security/hibp.py +91 -0
- svc_infra-0.1.595/src/svc_infra/security/jwt_rotation.py +53 -0
- svc_infra-0.1.595/src/svc_infra/security/lockout.py +96 -0
- svc_infra-0.1.595/src/svc_infra/security/models.py +245 -0
- svc_infra-0.1.595/src/svc_infra/security/org_invites.py +128 -0
- svc_infra-0.1.595/src/svc_infra/security/passwords.py +77 -0
- svc_infra-0.1.595/src/svc_infra/security/permissions.py +148 -0
- svc_infra-0.1.595/src/svc_infra/security/session.py +98 -0
- svc_infra-0.1.595/src/svc_infra/security/signed_cookies.py +80 -0
- svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/railway/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/alembic.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/models.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/provider/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/provider/base.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/provider/registry.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/provider/stripe.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/schemas.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/apf_payments/settings.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/apf_payments/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/apf_payments/setup.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/_cookies.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/models.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/pre_auth.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/router.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/security.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/mfa/verify.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/policy.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/providers.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/routers/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/routers/account.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/routers/apikey_router.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/security.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/sender.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/auth/state.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/cache/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/cache/add.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/http.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/nosql/mongo/add.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/nosql/mongo/health.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/add.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/crud_router.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/health.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/db/sql/session.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/docs/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/docs/landing.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/docs/scoped.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/dualize.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/protected.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/public.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/router.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dual/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/dx.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/ease.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/http/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/http/concurrency.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/http/conditional.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/http/deprecation.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/debug.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/errors/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/errors/catchall.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/errors/exceptions.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/errors/handlers.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/idempotency.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/middleware/request_id.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/apply.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/conventions.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/models.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/mutators.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/pipeline.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/responses.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/openapi/security.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/pagination.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/paths/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/paths/auth.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/paths/generic.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/paths/prefix.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/paths/user.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/routers/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/api/fastapi/routers/ping.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/env.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/logging/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/logging/add.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/logging/filter.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/logging/formats.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/app/root.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/backend.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/decorators.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/demo.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/keys.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/recache.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/resources.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/tags.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/ttl.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cache/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/sql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/sql/alembic_cmds.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/help.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/obs/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/cmds/obs/obs_cmds.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/foundation/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/foundation/runner.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/cli/foundation/typer_bootstrap.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/crud_schema.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/base.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/constants.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/core.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/indexes.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/management.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/client.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/settings.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/templates/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/templates/documents.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/templates/resources.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/mongo/templates/schemas.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/repository.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/resource.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/scaffold.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/service.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/service_with_hooks.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/types.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/nosql/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/apikey.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/authref.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/base.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/constants.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/core.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/management.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/repository.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/resource.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/scaffold.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/service.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/service_with_hooks.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/models_schemas/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/models_schemas/auth/schemas.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/models_schemas/entity/models.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/models_schemas/entity/schemas.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/setup/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/setup/alembic.ini.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/setup/env_async.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/setup/env_sync.py.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/templates/setup/script.py.mako.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/types.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/uniq.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/uniq_hooks.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/sql/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/db/utils.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/mcp/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/mcp/svc_infra_mcp.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/README.md +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/add.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/cloud_dash.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/metrics/asgi.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/metrics/base.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/metrics/http.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/metrics/sqlalchemy.py +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/metrics → svc_infra-0.1.595/src/svc_infra/obs/providers}/__init__.py +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers → svc_infra-0.1.595/src/svc_infra/obs/providers/compose_cloud}/__init__.py +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/compose_cloud → svc_infra-0.1.595/src/svc_infra/obs/providers/compose_cloud/templates}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/compose_cloud/templates/agent.yaml.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/compose_cloud/templates/docker-compose.cloud.yml.tmpl +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/compose_cloud/templates → svc_infra-0.1.595/src/svc_infra/obs/providers/grafana}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/dashboards/00_overview.json +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/dashboards/10_http.json +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/dashboards/20_db.json +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/dashboards/30_runtime.json +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/dashboards/40_clients.json +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana → svc_infra-0.1.595/src/svc_infra/obs/providers/grafana/dashboards}/__init__.py +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/dashboards → svc_infra-0.1.595/src/svc_infra/obs/providers/grafana/templates}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/templates/docker-compose.yml.tmpl +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/templates/prometheus.yml.tmpl +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/templates → svc_infra-0.1.595/src/svc_infra/obs/providers/grafana/templates/provisioning}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/templates/provisioning/dashboards.yml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/providers/grafana/templates/provisioning/datasource.yml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/settings.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/grafana_dashboard.json +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/prometheus_rules.yml +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/providers/grafana/templates/provisioning → svc_infra-0.1.595/src/svc_infra/obs/templates/sidecars}/__init__.py +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars → svc_infra-0.1.595/src/svc_infra/obs/templates/sidecars/compose}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/compose/agent.yaml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/compose/docker-compose.yml +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/compose → svc_infra-0.1.595/src/svc_infra/obs/templates/sidecars/fly}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/fly/agent.yaml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/fly/fly.toml.fragment +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/fly → svc_infra-0.1.595/src/svc_infra/obs/templates/sidecars/k8s}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/k8s/configmap.yaml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/k8s/deployment.yaml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/railway/Dockerfile +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/railway/README.md +0 -0
- {svc_infra-0.1.593/src/svc_infra/obs/templates/sidecars/k8s → svc_infra-0.1.595/src/svc_infra/obs/templates/sidecars/railway}/__init__.py +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/obs/templates/sidecars/railway/agent.yaml +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/py.typed +0 -0
- {svc_infra-0.1.593 → svc_infra-0.1.595}/src/svc_infra/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "svc-infra"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.595"
|
|
4
4
|
description = "Infrastructure for building and deploying prod-ready services"
|
|
5
5
|
authors = ["Ali Khatami <aliikhatami94@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -119,6 +119,10 @@ testpaths = ["tests"]
|
|
|
119
119
|
python_files = ["test_*.py", "*_test.py"]
|
|
120
120
|
python_classes = ["Test*",]
|
|
121
121
|
python_functions = ["test_*"]
|
|
122
|
+
markers = [
|
|
123
|
+
"security: Security and auth hardening tests",
|
|
124
|
+
"ratelimit: Rate limiting and abuse protection tests",
|
|
125
|
+
]
|
|
122
126
|
filterwarnings = [
|
|
123
127
|
"ignore:The `route` decorator is deprecated:DeprecationWarning:starlette.*",
|
|
124
128
|
]
|
|
@@ -111,6 +111,32 @@ app = setup_service_api(
|
|
|
111
111
|
add_payments(app, prefix="/payments")
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
**Tenant Context**
|
|
115
|
+
|
|
116
|
+
All payments endpoints require a tenant identifier. The FastAPI router now
|
|
117
|
+
derives it automatically from the authenticated principal:
|
|
118
|
+
|
|
119
|
+
- API key principals → ``principal.api_key.tenant_id``
|
|
120
|
+
- User principals → ``principal.user.tenant_id``
|
|
121
|
+
- Fallbacks: ``X-Tenant-Id`` request header or ``request.state.tenant_id``
|
|
122
|
+
|
|
123
|
+
If you need custom mapping logic (for example, translating API keys to an
|
|
124
|
+
external tenant registry), register an override during startup:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from svc_infra.api.fastapi.apf_payments.router import set_payments_tenant_resolver
|
|
128
|
+
|
|
129
|
+
async def resolve_tenant(request, identity, header):
|
|
130
|
+
# return a string tenant id, or None to fall back to the defaults
|
|
131
|
+
return "tenant-from-custom-logic"
|
|
132
|
+
|
|
133
|
+
set_payments_tenant_resolver(resolve_tenant)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If no tenant can be derived (and the override also returns ``None``), the
|
|
137
|
+
router responds with ``400 tenant_context_missing`` so callers can supply the
|
|
138
|
+
missing context explicitly.
|
|
139
|
+
|
|
114
140
|
**Environment-Based Configuration**
|
|
115
141
|
|
|
116
142
|
The `easy_service_app` reads these env vars automatically:
|
|
@@ -5,6 +5,7 @@ from datetime import date, datetime, timezone
|
|
|
5
5
|
from typing import Any, Optional, Sequence, Tuple
|
|
6
6
|
|
|
7
7
|
from svc_infra.apf_payments.schemas import (
|
|
8
|
+
BalanceAmount,
|
|
8
9
|
BalanceSnapshotOut,
|
|
9
10
|
CustomerOut,
|
|
10
11
|
CustomerUpsertIn,
|
|
@@ -277,13 +278,38 @@ def _usage_record_to_out(data: dict[str, Any]) -> UsageRecordOut:
|
|
|
277
278
|
provider_price_id=(
|
|
278
279
|
str(data.get("provider_price_id")) if data.get("provider_price_id") else None
|
|
279
280
|
),
|
|
281
|
+
action=(str(data.get("action")) if data.get("action") else None),
|
|
280
282
|
)
|
|
281
283
|
|
|
282
284
|
|
|
283
285
|
def _balance_snapshot_to_out(data: dict[str, Any]) -> BalanceSnapshotOut:
|
|
286
|
+
def _normalize(side: Any) -> list[dict[str, Any]]:
|
|
287
|
+
if isinstance(side, list):
|
|
288
|
+
out: list[dict[str, Any]] = []
|
|
289
|
+
for item in side:
|
|
290
|
+
if isinstance(item, dict) and "currency" in item and "amount" in item:
|
|
291
|
+
out.append(
|
|
292
|
+
{
|
|
293
|
+
"currency": str(item["currency"]).upper(),
|
|
294
|
+
"amount": int(item["amount"] or 0),
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
return out
|
|
298
|
+
if isinstance(side, dict):
|
|
299
|
+
return [
|
|
300
|
+
{"currency": str(cur).upper(), "amount": int(amt or 0)} for cur, amt in side.items()
|
|
301
|
+
]
|
|
302
|
+
return []
|
|
303
|
+
|
|
284
304
|
return BalanceSnapshotOut(
|
|
285
|
-
available=
|
|
286
|
-
|
|
305
|
+
available=[
|
|
306
|
+
BalanceAmount(currency=i["currency"], amount=i["amount"])
|
|
307
|
+
for i in _normalize(data.get("available"))
|
|
308
|
+
],
|
|
309
|
+
pending=[
|
|
310
|
+
BalanceAmount(currency=i["currency"], amount=i["amount"])
|
|
311
|
+
for i in _normalize(data.get("pending"))
|
|
312
|
+
],
|
|
287
313
|
)
|
|
288
314
|
|
|
289
315
|
|
|
@@ -65,9 +65,24 @@ def _default_provider_name() -> str:
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class PaymentsService:
|
|
68
|
+
"""Payments service facade wrapping provider adapters and persisting key rows.
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
NOTE: tenant_id is now required for all persistence operations. This is a breaking
|
|
71
|
+
change; callers must supply a valid tenant scope. (Future: could allow multi-tenant
|
|
72
|
+
mapping via adapter registry.)
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
session: AsyncSession,
|
|
78
|
+
*,
|
|
79
|
+
tenant_id: str,
|
|
80
|
+
provider_name: Optional[str] = None,
|
|
81
|
+
):
|
|
82
|
+
if not tenant_id:
|
|
83
|
+
raise ValueError("tenant_id is required for PaymentsService")
|
|
70
84
|
self.session = session
|
|
85
|
+
self.tenant_id = tenant_id
|
|
71
86
|
self._provider_name = (provider_name or _default_provider_name()).lower()
|
|
72
87
|
self._adapter = None # resolved on first use
|
|
73
88
|
|
|
@@ -118,6 +133,7 @@ class PaymentsService:
|
|
|
118
133
|
# If your PayCustomer model has additional columns (email/name), include them here.
|
|
119
134
|
self.session.add(
|
|
120
135
|
PayCustomer(
|
|
136
|
+
tenant_id=self.tenant_id,
|
|
121
137
|
provider=out.provider,
|
|
122
138
|
provider_customer_id=out.provider_customer_id,
|
|
123
139
|
user_id=data.user_id,
|
|
@@ -132,6 +148,7 @@ class PaymentsService:
|
|
|
132
148
|
out = await adapter.create_intent(data, user_id=user_id)
|
|
133
149
|
self.session.add(
|
|
134
150
|
PayIntent(
|
|
151
|
+
tenant_id=self.tenant_id,
|
|
135
152
|
provider=out.provider,
|
|
136
153
|
provider_intent_id=out.provider_intent_id,
|
|
137
154
|
user_id=user_id,
|
|
@@ -167,6 +184,32 @@ class PaymentsService:
|
|
|
167
184
|
async def refund(self, provider_intent_id: str, data: RefundIn) -> IntentOut:
|
|
168
185
|
adapter = self._get_adapter()
|
|
169
186
|
out = await adapter.refund(provider_intent_id, data)
|
|
187
|
+
# Create ledger entry if amount present and not already recorded
|
|
188
|
+
pi = await self.session.scalar(
|
|
189
|
+
select(PayIntent).where(PayIntent.provider_intent_id == provider_intent_id)
|
|
190
|
+
)
|
|
191
|
+
if pi:
|
|
192
|
+
amount = int(data.amount) if data.amount is not None else out.amount
|
|
193
|
+
# Guard against duplicates (same provider_ref + kind)
|
|
194
|
+
existing = await self.session.scalar(
|
|
195
|
+
select(LedgerEntry).where(
|
|
196
|
+
LedgerEntry.provider_ref == provider_intent_id,
|
|
197
|
+
LedgerEntry.kind == "refund",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
if amount > 0 and not existing:
|
|
201
|
+
self.session.add(
|
|
202
|
+
LedgerEntry(
|
|
203
|
+
tenant_id=self.tenant_id,
|
|
204
|
+
provider=pi.provider,
|
|
205
|
+
provider_ref=provider_intent_id,
|
|
206
|
+
user_id=pi.user_id,
|
|
207
|
+
amount=+amount,
|
|
208
|
+
currency=out.currency,
|
|
209
|
+
kind="refund",
|
|
210
|
+
status="posted",
|
|
211
|
+
)
|
|
212
|
+
)
|
|
170
213
|
return out
|
|
171
214
|
|
|
172
215
|
# --- Webhooks -------------------------------------------------------------
|
|
@@ -176,6 +219,7 @@ class PaymentsService:
|
|
|
176
219
|
parsed = await adapter.verify_and_parse_webhook(signature, payload)
|
|
177
220
|
self.session.add(
|
|
178
221
|
PayEvent(
|
|
222
|
+
tenant_id=self.tenant_id,
|
|
179
223
|
provider=provider,
|
|
180
224
|
provider_event_id=parsed["id"],
|
|
181
225
|
type=parsed.get("type", ""),
|
|
@@ -199,6 +243,7 @@ class PaymentsService:
|
|
|
199
243
|
intent.status = "succeeded"
|
|
200
244
|
self.session.add(
|
|
201
245
|
LedgerEntry(
|
|
246
|
+
tenant_id=self.tenant_id,
|
|
202
247
|
provider=intent.provider,
|
|
203
248
|
provider_ref=provider_intent_id,
|
|
204
249
|
user_id=intent.user_id,
|
|
@@ -217,17 +262,27 @@ class PaymentsService:
|
|
|
217
262
|
select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
|
|
218
263
|
)
|
|
219
264
|
if intent:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
provider_ref
|
|
224
|
-
|
|
225
|
-
amount=+amount,
|
|
226
|
-
currency=currency,
|
|
227
|
-
kind="capture",
|
|
228
|
-
status="posted",
|
|
265
|
+
# Avoid duplicate capture entries
|
|
266
|
+
existing = await self.session.scalar(
|
|
267
|
+
select(LedgerEntry).where(
|
|
268
|
+
LedgerEntry.provider_ref == charge_obj.get("id"),
|
|
269
|
+
LedgerEntry.kind == "capture",
|
|
229
270
|
)
|
|
230
271
|
)
|
|
272
|
+
if not existing:
|
|
273
|
+
self.session.add(
|
|
274
|
+
LedgerEntry(
|
|
275
|
+
tenant_id=self.tenant_id,
|
|
276
|
+
provider=intent.provider,
|
|
277
|
+
provider_ref=charge_obj.get("id"),
|
|
278
|
+
user_id=intent.user_id,
|
|
279
|
+
amount=+amount,
|
|
280
|
+
currency=currency,
|
|
281
|
+
kind="capture",
|
|
282
|
+
status="posted",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
intent.captured = True
|
|
231
286
|
|
|
232
287
|
async def _post_refund(self, charge_obj: dict):
|
|
233
288
|
amount = int(charge_obj.get("amount_refunded") or 0)
|
|
@@ -237,22 +292,31 @@ class PaymentsService:
|
|
|
237
292
|
select(PayIntent).where(PayIntent.provider_intent_id == pi_id)
|
|
238
293
|
)
|
|
239
294
|
if intent and amount > 0:
|
|
240
|
-
self.session.
|
|
241
|
-
LedgerEntry(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
user_id=intent.user_id,
|
|
245
|
-
amount=+amount,
|
|
246
|
-
currency=currency,
|
|
247
|
-
kind="refund",
|
|
248
|
-
status="posted",
|
|
295
|
+
existing = await self.session.scalar(
|
|
296
|
+
select(LedgerEntry).where(
|
|
297
|
+
LedgerEntry.provider_ref == charge_obj.get("id"),
|
|
298
|
+
LedgerEntry.kind == "refund",
|
|
249
299
|
)
|
|
250
300
|
)
|
|
301
|
+
if not existing:
|
|
302
|
+
self.session.add(
|
|
303
|
+
LedgerEntry(
|
|
304
|
+
tenant_id=self.tenant_id,
|
|
305
|
+
provider=intent.provider,
|
|
306
|
+
provider_ref=charge_obj.get("id"),
|
|
307
|
+
user_id=intent.user_id,
|
|
308
|
+
amount=+amount,
|
|
309
|
+
currency=currency,
|
|
310
|
+
kind="refund",
|
|
311
|
+
status="posted",
|
|
312
|
+
)
|
|
313
|
+
)
|
|
251
314
|
|
|
252
315
|
async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
|
|
253
316
|
out = await self._get_adapter().attach_payment_method(data)
|
|
254
317
|
# Upsert locally for quick listing
|
|
255
318
|
pm = PayPaymentMethod(
|
|
319
|
+
tenant_id=self.tenant_id,
|
|
256
320
|
provider=out.provider,
|
|
257
321
|
provider_customer_id=out.provider_customer_id,
|
|
258
322
|
provider_method_id=out.provider_method_id,
|
|
@@ -283,6 +347,7 @@ class PaymentsService:
|
|
|
283
347
|
out = await self._get_adapter().create_product(data)
|
|
284
348
|
self.session.add(
|
|
285
349
|
PayProduct(
|
|
350
|
+
tenant_id=self.tenant_id,
|
|
286
351
|
provider=out.provider,
|
|
287
352
|
provider_product_id=out.provider_product_id,
|
|
288
353
|
name=out.name,
|
|
@@ -295,6 +360,7 @@ class PaymentsService:
|
|
|
295
360
|
out = await self._get_adapter().create_price(data)
|
|
296
361
|
self.session.add(
|
|
297
362
|
PayPrice(
|
|
363
|
+
tenant_id=self.tenant_id,
|
|
298
364
|
provider=out.provider,
|
|
299
365
|
provider_price_id=out.provider_price_id,
|
|
300
366
|
provider_product_id=out.provider_product_id,
|
|
@@ -312,6 +378,7 @@ class PaymentsService:
|
|
|
312
378
|
out = await self._get_adapter().create_subscription(data)
|
|
313
379
|
self.session.add(
|
|
314
380
|
PaySubscription(
|
|
381
|
+
tenant_id=self.tenant_id,
|
|
315
382
|
provider=out.provider,
|
|
316
383
|
provider_subscription_id=out.provider_subscription_id,
|
|
317
384
|
provider_price_id=out.provider_price_id,
|
|
@@ -340,6 +407,7 @@ class PaymentsService:
|
|
|
340
407
|
out = await self._get_adapter().create_invoice(data)
|
|
341
408
|
self.session.add(
|
|
342
409
|
PayInvoice(
|
|
410
|
+
tenant_id=self.tenant_id,
|
|
343
411
|
provider=out.provider,
|
|
344
412
|
provider_invoice_id=out.provider_invoice_id,
|
|
345
413
|
provider_customer_id=out.provider_customer_id,
|
|
@@ -432,6 +500,27 @@ class PaymentsService:
|
|
|
432
500
|
pi.status = out.status
|
|
433
501
|
if out.status in ("succeeded", "requires_capture"): # Stripe specifics vary
|
|
434
502
|
pi.captured = True if out.status == "succeeded" else pi.captured
|
|
503
|
+
# Add capture ledger entry if succeeded and not already posted
|
|
504
|
+
if out.status == "succeeded":
|
|
505
|
+
existing = await self.session.scalar(
|
|
506
|
+
select(LedgerEntry).where(
|
|
507
|
+
LedgerEntry.provider_ref == provider_intent_id,
|
|
508
|
+
LedgerEntry.kind == "capture",
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
if not existing:
|
|
512
|
+
self.session.add(
|
|
513
|
+
LedgerEntry(
|
|
514
|
+
tenant_id=self.tenant_id,
|
|
515
|
+
provider=pi.provider,
|
|
516
|
+
provider_ref=provider_intent_id,
|
|
517
|
+
user_id=pi.user_id,
|
|
518
|
+
amount=+out.amount,
|
|
519
|
+
currency=out.currency,
|
|
520
|
+
kind="capture",
|
|
521
|
+
status="posted",
|
|
522
|
+
)
|
|
523
|
+
)
|
|
435
524
|
return out
|
|
436
525
|
|
|
437
526
|
async def list_intents(self, f: IntentListFilter) -> tuple[list[IntentOut], str | None]:
|
|
@@ -475,6 +564,7 @@ class PaymentsService:
|
|
|
475
564
|
out = await self._get_adapter().create_setup_intent(data)
|
|
476
565
|
self.session.add(
|
|
477
566
|
PaySetupIntent(
|
|
567
|
+
tenant_id=self.tenant_id,
|
|
478
568
|
provider=out.provider,
|
|
479
569
|
provider_setup_intent_id=out.provider_setup_intent_id,
|
|
480
570
|
user_id=None,
|
|
@@ -510,6 +600,7 @@ class PaymentsService:
|
|
|
510
600
|
else:
|
|
511
601
|
self.session.add(
|
|
512
602
|
PaySetupIntent(
|
|
603
|
+
tenant_id=self.tenant_id,
|
|
513
604
|
provider=out.provider,
|
|
514
605
|
provider_setup_intent_id=out.provider_setup_intent_id,
|
|
515
606
|
user_id=None,
|
|
@@ -549,6 +640,7 @@ class PaymentsService:
|
|
|
549
640
|
else:
|
|
550
641
|
self.session.add(
|
|
551
642
|
PayDispute(
|
|
643
|
+
tenant_id=self.tenant_id,
|
|
552
644
|
provider=out.provider,
|
|
553
645
|
provider_dispute_id=out.provider_dispute_id,
|
|
554
646
|
provider_charge_id=None, # set if adapter returns it
|
|
@@ -594,6 +686,7 @@ class PaymentsService:
|
|
|
594
686
|
else:
|
|
595
687
|
self.session.add(
|
|
596
688
|
PayPayout(
|
|
689
|
+
tenant_id=self.tenant_id,
|
|
597
690
|
provider=out.provider,
|
|
598
691
|
provider_payout_id=out.provider_payout_id,
|
|
599
692
|
amount=out.amount,
|
|
@@ -678,10 +771,10 @@ class PaymentsService:
|
|
|
678
771
|
if not row:
|
|
679
772
|
self.session.add(
|
|
680
773
|
PayCustomer(
|
|
774
|
+
tenant_id=self.tenant_id,
|
|
681
775
|
provider=out.provider,
|
|
682
776
|
provider_customer_id=out.provider_customer_id,
|
|
683
777
|
user_id=None,
|
|
684
|
-
tenant_id="",
|
|
685
778
|
)
|
|
686
779
|
)
|
|
687
780
|
return out
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Annotated, Awaitable, Callable, Literal, Optional, cast
|
|
4
5
|
|
|
5
|
-
from fastapi import Body, Depends, Header, Request, Response, status
|
|
6
|
+
from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
|
|
6
7
|
from starlette.responses import JSONResponse
|
|
7
8
|
|
|
8
9
|
from svc_infra.apf_payments.schemas import (
|
|
@@ -47,6 +48,7 @@ from svc_infra.apf_payments.schemas import (
|
|
|
47
48
|
WebhookReplayOut,
|
|
48
49
|
)
|
|
49
50
|
from svc_infra.apf_payments.service import PaymentsService
|
|
51
|
+
from svc_infra.api.fastapi.auth.security import OptionalIdentity, Principal
|
|
50
52
|
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
51
53
|
from svc_infra.api.fastapi.dual import protected_router, public_router, service_router, user_router
|
|
52
54
|
from svc_infra.api.fastapi.dual.router import DualAPIRouter
|
|
@@ -69,8 +71,69 @@ def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "captur
|
|
|
69
71
|
|
|
70
72
|
|
|
71
73
|
# --- deps ---
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
TenantOverrideHook = Callable[
|
|
75
|
+
[Request, Optional[Principal], Optional[str]],
|
|
76
|
+
Awaitable[Optional[str]] | Optional[str],
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
_tenant_override_hook: TenantOverrideHook | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def set_payments_tenant_resolver(resolver: TenantOverrideHook | None) -> None:
|
|
83
|
+
"""Override the default tenant resolution used by the payments router.
|
|
84
|
+
|
|
85
|
+
Projects can call this during startup to plug custom logic (e.g. multi-tenant
|
|
86
|
+
mappings). Passing ``None`` resets to the built-in behavior.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
global _tenant_override_hook
|
|
90
|
+
_tenant_override_hook = resolver
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def resolve_payments_tenant_id(
|
|
94
|
+
request: Request,
|
|
95
|
+
identity: OptionalIdentity = None,
|
|
96
|
+
tenant_header: Annotated[Optional[str], Header(alias="X-Tenant-Id", default=None)] = None,
|
|
97
|
+
) -> str:
|
|
98
|
+
"""Determine the tenant id for the current request.
|
|
99
|
+
|
|
100
|
+
The default strategy prefers authenticated principals (API keys first, then
|
|
101
|
+
user accounts) and falls back to the ``X-Tenant-Id`` header or ``request.state``.
|
|
102
|
+
Applications may override the behavior via
|
|
103
|
+
:func:`set_payments_tenant_resolver`.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if _tenant_override_hook is not None:
|
|
107
|
+
maybe = _tenant_override_hook(request, identity, tenant_header)
|
|
108
|
+
if inspect.isawaitable(maybe): # pragma: no cover - depends on override type
|
|
109
|
+
maybe = await maybe
|
|
110
|
+
if maybe is not None:
|
|
111
|
+
return maybe
|
|
112
|
+
|
|
113
|
+
if identity:
|
|
114
|
+
api_key_tenant = getattr(getattr(identity, "api_key", None), "tenant_id", None)
|
|
115
|
+
if api_key_tenant:
|
|
116
|
+
return api_key_tenant
|
|
117
|
+
|
|
118
|
+
user_tenant = getattr(getattr(identity, "user", None), "tenant_id", None)
|
|
119
|
+
if user_tenant:
|
|
120
|
+
return user_tenant
|
|
121
|
+
|
|
122
|
+
if tenant_header:
|
|
123
|
+
return tenant_header
|
|
124
|
+
|
|
125
|
+
state_tenant = getattr(request.state, "tenant_id", None)
|
|
126
|
+
if state_tenant:
|
|
127
|
+
return state_tenant
|
|
128
|
+
|
|
129
|
+
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="tenant_context_missing")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
PaymentsTenantDep = Annotated[str, Depends(resolve_payments_tenant_id)]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def get_service(session: SqlSessionDep, tenant_id: PaymentsTenantDep) -> PaymentsService:
|
|
136
|
+
return PaymentsService(session=session, tenant_id=tenant_id)
|
|
74
137
|
|
|
75
138
|
|
|
76
139
|
# --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
|
|
@@ -11,6 +11,7 @@ from svc_infra.api.fastapi.auth.mfa.router import mfa_router
|
|
|
11
11
|
from svc_infra.api.fastapi.auth.routers.account import account_router
|
|
12
12
|
from svc_infra.api.fastapi.auth.routers.apikey_router import apikey_router
|
|
13
13
|
from svc_infra.api.fastapi.auth.routers.oauth_router import oauth_router_with_backend
|
|
14
|
+
from svc_infra.api.fastapi.auth.routers.session_router import build_session_router
|
|
14
15
|
from svc_infra.api.fastapi.db.sql.users import get_fastapi_users
|
|
15
16
|
from svc_infra.api.fastapi.paths.prefix import AUTH_PREFIX, USER_PREFIX
|
|
16
17
|
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV
|
|
@@ -73,6 +74,15 @@ def install_user_routers(
|
|
|
73
74
|
include_in_schema=include_in_docs,
|
|
74
75
|
dependencies=[Depends(login_client_gaurd)],
|
|
75
76
|
)
|
|
77
|
+
# Session/device listing & revocation endpoints (AuthSession model)
|
|
78
|
+
# Mounted under the user prefix so final paths become /{user_prefix}/sessions/... (e.g., /users/sessions/me)
|
|
79
|
+
# The router itself has a /sessions prefix.
|
|
80
|
+
app.include_router(
|
|
81
|
+
build_session_router(),
|
|
82
|
+
prefix=user_prefix,
|
|
83
|
+
tags=["Session Management"],
|
|
84
|
+
include_in_schema=include_in_docs,
|
|
85
|
+
)
|
|
76
86
|
app.include_router(
|
|
77
87
|
register_router,
|
|
78
88
|
prefix=user_prefix,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
from datetime import datetime, timezone
|
|
4
5
|
|
|
5
6
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
|
@@ -65,6 +66,9 @@ def auth_session_router(
|
|
|
65
66
|
router = public_router()
|
|
66
67
|
policy = auth_policy or DefaultAuthPolicy(get_auth_settings())
|
|
67
68
|
|
|
69
|
+
from svc_infra.api.fastapi.db.sql import SqlSessionDep
|
|
70
|
+
from svc_infra.security.lockout import get_lockout_status, record_attempt
|
|
71
|
+
|
|
68
72
|
@router.post("/login", name="auth:jwt.login")
|
|
69
73
|
async def login(
|
|
70
74
|
request: Request,
|
|
@@ -74,27 +78,78 @@ def auth_session_router(
|
|
|
74
78
|
client_id: str | None = Form(None),
|
|
75
79
|
client_secret: str | None = Form(None),
|
|
76
80
|
user_manager=Depends(fapi.get_user_manager),
|
|
81
|
+
session: SqlSessionDep = Depends(),
|
|
77
82
|
):
|
|
78
|
-
# 1) lookup user (normalize email)
|
|
79
83
|
strategy = auth_backend.get_strategy()
|
|
80
|
-
|
|
81
84
|
email = username.strip().lower()
|
|
85
|
+
# Compute IP hash for lockout correlation
|
|
86
|
+
client_ip = getattr(request.client, "host", None)
|
|
87
|
+
ip_hash = hashlib.sha256(client_ip.encode()).hexdigest() if client_ip else None
|
|
88
|
+
|
|
89
|
+
# Pre-check lockout by IP to avoid enumeration
|
|
90
|
+
try:
|
|
91
|
+
status_lo = await get_lockout_status(session, user_id=None, ip_hash=ip_hash)
|
|
92
|
+
if status_lo.locked and status_lo.next_allowed_at:
|
|
93
|
+
retry = int(
|
|
94
|
+
(status_lo.next_allowed_at - datetime.now(timezone.utc)).total_seconds()
|
|
95
|
+
)
|
|
96
|
+
raise HTTPException(
|
|
97
|
+
status_code=429,
|
|
98
|
+
detail="account_locked",
|
|
99
|
+
headers={"Retry-After": str(max(0, retry))},
|
|
100
|
+
)
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
# Lookup user
|
|
82
105
|
user = await user_manager.user_db.get_by_email(email)
|
|
83
106
|
if not user:
|
|
84
107
|
_, _ = _pwd.verify_and_update(password, _DUMMY_BCRYPT)
|
|
108
|
+
try:
|
|
109
|
+
await record_attempt(session, user_id=None, ip_hash=ip_hash, success=False)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
85
112
|
raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
|
|
86
113
|
|
|
87
|
-
#
|
|
114
|
+
# Status checks
|
|
88
115
|
if not getattr(user, "is_active", True):
|
|
89
116
|
raise HTTPException(401, "account_disabled")
|
|
90
117
|
|
|
91
118
|
hashed = getattr(user, "hashed_password", None) or getattr(user, "password_hash", None)
|
|
92
119
|
if not hashed:
|
|
93
|
-
|
|
120
|
+
try:
|
|
121
|
+
await record_attempt(
|
|
122
|
+
session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=False
|
|
123
|
+
)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
94
126
|
raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
|
|
95
127
|
|
|
128
|
+
# Check lockout for this user + IP before verifying password
|
|
129
|
+
try:
|
|
130
|
+
status_user = await get_lockout_status(
|
|
131
|
+
session, user_id=getattr(user, "id", None), ip_hash=ip_hash
|
|
132
|
+
)
|
|
133
|
+
if status_user.locked and status_user.next_allowed_at:
|
|
134
|
+
retry = int(
|
|
135
|
+
(status_user.next_allowed_at - datetime.now(timezone.utc)).total_seconds()
|
|
136
|
+
)
|
|
137
|
+
raise HTTPException(
|
|
138
|
+
status_code=429,
|
|
139
|
+
detail="account_locked",
|
|
140
|
+
headers={"Retry-After": str(max(0, retry))},
|
|
141
|
+
)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
96
145
|
ok, new_hash = _pwd.verify_and_update(password, hashed)
|
|
97
146
|
if not ok:
|
|
147
|
+
try:
|
|
148
|
+
await record_attempt(
|
|
149
|
+
session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=False
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
98
153
|
raise HTTPException(400, "LOGIN_BAD_CREDENTIALS")
|
|
99
154
|
|
|
100
155
|
# If the hash needs upgrading, persist it (optional but recommended)
|
|
@@ -106,7 +161,6 @@ def auth_session_router(
|
|
|
106
161
|
try:
|
|
107
162
|
await user_manager.user_db.update(user)
|
|
108
163
|
except Exception:
|
|
109
|
-
# don't block login if updating hash fails; log if you have logging here
|
|
110
164
|
pass
|
|
111
165
|
|
|
112
166
|
if getattr(user, "is_verified") is False:
|
|
@@ -130,6 +184,14 @@ def auth_session_router(
|
|
|
130
184
|
# don’t block login if this write fails
|
|
131
185
|
pass
|
|
132
186
|
|
|
187
|
+
# Record successful attempt (for audit)
|
|
188
|
+
try:
|
|
189
|
+
await record_attempt(
|
|
190
|
+
session, user_id=getattr(user, "id", None), ip_hash=ip_hash, success=True
|
|
191
|
+
)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
133
195
|
# 5) mint token and set cookie
|
|
134
196
|
token = await strategy.write_token(user)
|
|
135
197
|
st = get_auth_settings()
|