svc-infra 0.1.595__py3-none-any.whl → 1.1.0__py3-none-any.whl
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/__init__.py +58 -2
- svc_infra/apf_payments/models.py +68 -38
- svc_infra/apf_payments/provider/__init__.py +2 -2
- svc_infra/apf_payments/provider/aiydan.py +39 -23
- svc_infra/apf_payments/provider/base.py +8 -3
- svc_infra/apf_payments/provider/registry.py +3 -5
- svc_infra/apf_payments/provider/stripe.py +74 -52
- svc_infra/apf_payments/schemas.py +84 -83
- svc_infra/apf_payments/service.py +27 -16
- svc_infra/apf_payments/settings.py +12 -11
- svc_infra/api/__init__.py +61 -0
- svc_infra/api/fastapi/__init__.py +34 -0
- svc_infra/api/fastapi/admin/__init__.py +3 -0
- svc_infra/api/fastapi/admin/add.py +240 -0
- svc_infra/api/fastapi/apf_payments/router.py +94 -73
- svc_infra/api/fastapi/apf_payments/setup.py +10 -9
- svc_infra/api/fastapi/auth/__init__.py +65 -0
- svc_infra/api/fastapi/auth/_cookies.py +1 -3
- svc_infra/api/fastapi/auth/add.py +14 -15
- svc_infra/api/fastapi/auth/gaurd.py +32 -20
- svc_infra/api/fastapi/auth/mfa/models.py +3 -4
- svc_infra/api/fastapi/auth/mfa/pre_auth.py +13 -9
- svc_infra/api/fastapi/auth/mfa/router.py +9 -8
- svc_infra/api/fastapi/auth/mfa/security.py +4 -7
- svc_infra/api/fastapi/auth/mfa/utils.py +5 -3
- svc_infra/api/fastapi/auth/policy.py +0 -1
- svc_infra/api/fastapi/auth/providers.py +3 -3
- svc_infra/api/fastapi/auth/routers/apikey_router.py +19 -21
- svc_infra/api/fastapi/auth/routers/oauth_router.py +98 -52
- svc_infra/api/fastapi/auth/routers/session_router.py +6 -5
- svc_infra/api/fastapi/auth/security.py +25 -15
- svc_infra/api/fastapi/auth/sender.py +5 -0
- svc_infra/api/fastapi/auth/settings.py +18 -19
- svc_infra/api/fastapi/auth/state.py +5 -4
- svc_infra/api/fastapi/auth/ws_security.py +275 -0
- svc_infra/api/fastapi/billing/router.py +71 -0
- svc_infra/api/fastapi/billing/setup.py +19 -0
- svc_infra/api/fastapi/cache/add.py +9 -5
- svc_infra/api/fastapi/db/__init__.py +5 -1
- svc_infra/api/fastapi/db/http.py +10 -9
- svc_infra/api/fastapi/db/nosql/__init__.py +39 -1
- svc_infra/api/fastapi/db/nosql/mongo/add.py +35 -30
- svc_infra/api/fastapi/db/nosql/mongo/crud_router.py +39 -21
- svc_infra/api/fastapi/db/sql/__init__.py +5 -1
- svc_infra/api/fastapi/db/sql/add.py +62 -25
- svc_infra/api/fastapi/db/sql/crud_router.py +205 -30
- svc_infra/api/fastapi/db/sql/session.py +19 -2
- svc_infra/api/fastapi/db/sql/users.py +18 -9
- svc_infra/api/fastapi/dependencies/ratelimit.py +76 -14
- svc_infra/api/fastapi/docs/add.py +163 -0
- svc_infra/api/fastapi/docs/landing.py +6 -6
- svc_infra/api/fastapi/docs/scoped.py +75 -36
- svc_infra/api/fastapi/dual/__init__.py +12 -2
- svc_infra/api/fastapi/dual/dualize.py +2 -2
- svc_infra/api/fastapi/dual/protected.py +123 -10
- svc_infra/api/fastapi/dual/public.py +25 -0
- svc_infra/api/fastapi/dual/router.py +18 -8
- svc_infra/api/fastapi/dx.py +33 -2
- svc_infra/api/fastapi/ease.py +59 -7
- svc_infra/api/fastapi/http/concurrency.py +2 -1
- svc_infra/api/fastapi/http/conditional.py +2 -2
- svc_infra/api/fastapi/middleware/debug.py +4 -1
- svc_infra/api/fastapi/middleware/errors/exceptions.py +2 -5
- svc_infra/api/fastapi/middleware/errors/handlers.py +50 -10
- svc_infra/api/fastapi/middleware/graceful_shutdown.py +95 -0
- svc_infra/api/fastapi/middleware/idempotency.py +190 -68
- svc_infra/api/fastapi/middleware/idempotency_store.py +187 -0
- svc_infra/api/fastapi/middleware/optimistic_lock.py +39 -0
- svc_infra/api/fastapi/middleware/ratelimit.py +125 -28
- svc_infra/api/fastapi/middleware/ratelimit_store.py +45 -13
- svc_infra/api/fastapi/middleware/request_id.py +24 -10
- svc_infra/api/fastapi/middleware/request_size_limit.py +3 -3
- svc_infra/api/fastapi/middleware/timeout.py +176 -0
- svc_infra/api/fastapi/object_router.py +1060 -0
- svc_infra/api/fastapi/openapi/apply.py +4 -3
- svc_infra/api/fastapi/openapi/conventions.py +13 -6
- svc_infra/api/fastapi/openapi/mutators.py +144 -17
- svc_infra/api/fastapi/openapi/pipeline.py +2 -2
- svc_infra/api/fastapi/openapi/responses.py +4 -6
- svc_infra/api/fastapi/openapi/security.py +1 -1
- svc_infra/api/fastapi/ops/add.py +73 -0
- svc_infra/api/fastapi/pagination.py +47 -32
- svc_infra/api/fastapi/routers/__init__.py +16 -10
- svc_infra/api/fastapi/routers/ping.py +1 -0
- svc_infra/api/fastapi/setup.py +167 -54
- svc_infra/api/fastapi/tenancy/add.py +20 -0
- svc_infra/api/fastapi/tenancy/context.py +113 -0
- svc_infra/api/fastapi/versioned.py +102 -0
- svc_infra/app/README.md +5 -5
- svc_infra/app/__init__.py +3 -1
- svc_infra/app/env.py +70 -4
- svc_infra/app/logging/add.py +10 -2
- svc_infra/app/logging/filter.py +1 -1
- svc_infra/app/logging/formats.py +13 -5
- svc_infra/app/root.py +3 -3
- svc_infra/billing/__init__.py +40 -0
- svc_infra/billing/async_service.py +167 -0
- svc_infra/billing/jobs.py +231 -0
- svc_infra/billing/models.py +146 -0
- svc_infra/billing/quotas.py +101 -0
- svc_infra/billing/schemas.py +34 -0
- svc_infra/bundled_docs/README.md +5 -0
- svc_infra/bundled_docs/__init__.py +1 -0
- svc_infra/bundled_docs/getting-started.md +6 -0
- svc_infra/cache/__init__.py +21 -5
- svc_infra/cache/add.py +167 -0
- svc_infra/cache/backend.py +9 -7
- svc_infra/cache/decorators.py +75 -20
- svc_infra/cache/demo.py +2 -2
- svc_infra/cache/keys.py +26 -6
- svc_infra/cache/recache.py +26 -27
- svc_infra/cache/resources.py +6 -5
- svc_infra/cache/tags.py +19 -44
- svc_infra/cache/ttl.py +2 -3
- svc_infra/cache/utils.py +4 -3
- svc_infra/cli/__init__.py +44 -8
- svc_infra/cli/__main__.py +4 -0
- svc_infra/cli/cmds/__init__.py +39 -2
- svc_infra/cli/cmds/db/nosql/mongo/mongo_cmds.py +18 -14
- svc_infra/cli/cmds/db/nosql/mongo/mongo_scaffold_cmds.py +9 -10
- svc_infra/cli/cmds/db/ops_cmds.py +267 -0
- svc_infra/cli/cmds/db/sql/alembic_cmds.py +97 -29
- svc_infra/cli/cmds/db/sql/sql_export_cmds.py +80 -0
- svc_infra/cli/cmds/db/sql/sql_scaffold_cmds.py +13 -13
- svc_infra/cli/cmds/docs/docs_cmds.py +139 -0
- svc_infra/cli/cmds/dx/__init__.py +12 -0
- svc_infra/cli/cmds/dx/dx_cmds.py +110 -0
- svc_infra/cli/cmds/health/__init__.py +179 -0
- svc_infra/cli/cmds/health/health_cmds.py +8 -0
- svc_infra/cli/cmds/help.py +4 -0
- svc_infra/cli/cmds/jobs/__init__.py +1 -0
- svc_infra/cli/cmds/jobs/jobs_cmds.py +42 -0
- svc_infra/cli/cmds/obs/obs_cmds.py +31 -13
- svc_infra/cli/cmds/sdk/__init__.py +0 -0
- svc_infra/cli/cmds/sdk/sdk_cmds.py +102 -0
- svc_infra/cli/foundation/runner.py +4 -5
- svc_infra/cli/foundation/typer_bootstrap.py +1 -2
- svc_infra/data/__init__.py +83 -0
- svc_infra/data/add.py +61 -0
- svc_infra/data/backup.py +56 -0
- svc_infra/data/erasure.py +46 -0
- svc_infra/data/fixtures.py +42 -0
- svc_infra/data/retention.py +56 -0
- svc_infra/db/__init__.py +15 -0
- svc_infra/db/crud_schema.py +14 -13
- svc_infra/db/inbox.py +67 -0
- svc_infra/db/nosql/__init__.py +2 -0
- svc_infra/db/nosql/constants.py +1 -1
- svc_infra/db/nosql/core.py +19 -5
- svc_infra/db/nosql/indexes.py +12 -9
- svc_infra/db/nosql/management.py +4 -4
- svc_infra/db/nosql/mongo/README.md +13 -13
- svc_infra/db/nosql/mongo/client.py +21 -4
- svc_infra/db/nosql/mongo/settings.py +1 -1
- svc_infra/db/nosql/repository.py +46 -27
- svc_infra/db/nosql/resource.py +28 -16
- svc_infra/db/nosql/scaffold.py +14 -12
- svc_infra/db/nosql/service.py +2 -1
- svc_infra/db/nosql/service_with_hooks.py +4 -3
- svc_infra/db/nosql/utils.py +4 -4
- svc_infra/db/ops.py +380 -0
- svc_infra/db/outbox.py +105 -0
- svc_infra/db/sql/apikey.py +34 -15
- svc_infra/db/sql/authref.py +8 -6
- svc_infra/db/sql/constants.py +5 -1
- svc_infra/db/sql/core.py +13 -13
- svc_infra/db/sql/management.py +5 -6
- svc_infra/db/sql/repository.py +92 -26
- svc_infra/db/sql/resource.py +18 -12
- svc_infra/db/sql/scaffold.py +11 -11
- svc_infra/db/sql/service.py +2 -1
- svc_infra/db/sql/service_with_hooks.py +4 -3
- svc_infra/db/sql/templates/models_schemas/auth/models.py.tmpl +7 -56
- svc_infra/db/sql/templates/setup/env_async.py.tmpl +34 -12
- svc_infra/db/sql/templates/setup/env_sync.py.tmpl +29 -7
- svc_infra/db/sql/tenant.py +80 -0
- svc_infra/db/sql/uniq.py +8 -7
- svc_infra/db/sql/uniq_hooks.py +12 -11
- svc_infra/db/sql/utils.py +105 -47
- svc_infra/db/sql/versioning.py +14 -0
- svc_infra/db/utils.py +3 -3
- svc_infra/deploy/__init__.py +531 -0
- svc_infra/documents/__init__.py +100 -0
- svc_infra/documents/add.py +263 -0
- svc_infra/documents/ease.py +233 -0
- svc_infra/documents/models.py +114 -0
- svc_infra/documents/storage.py +262 -0
- svc_infra/dx/__init__.py +58 -0
- svc_infra/dx/add.py +63 -0
- svc_infra/dx/changelog.py +74 -0
- svc_infra/dx/checks.py +68 -0
- svc_infra/exceptions.py +141 -0
- svc_infra/health/__init__.py +863 -0
- svc_infra/http/__init__.py +13 -0
- svc_infra/http/client.py +101 -0
- svc_infra/jobs/__init__.py +79 -0
- svc_infra/jobs/builtins/outbox_processor.py +38 -0
- svc_infra/jobs/builtins/webhook_delivery.py +93 -0
- svc_infra/jobs/easy.py +33 -0
- svc_infra/jobs/loader.py +49 -0
- svc_infra/jobs/queue.py +106 -0
- svc_infra/jobs/redis_queue.py +242 -0
- svc_infra/jobs/runner.py +75 -0
- svc_infra/jobs/scheduler.py +53 -0
- svc_infra/jobs/worker.py +40 -0
- svc_infra/loaders/__init__.py +186 -0
- svc_infra/loaders/base.py +143 -0
- svc_infra/loaders/github.py +309 -0
- svc_infra/loaders/models.py +147 -0
- svc_infra/loaders/url.py +229 -0
- svc_infra/logging/__init__.py +375 -0
- svc_infra/mcp/__init__.py +82 -0
- svc_infra/mcp/svc_infra_mcp.py +91 -33
- svc_infra/obs/README.md +2 -0
- svc_infra/obs/add.py +68 -11
- svc_infra/obs/cloud_dash.py +2 -1
- svc_infra/obs/grafana/dashboards/http-overview.json +45 -0
- svc_infra/obs/metrics/__init__.py +6 -7
- svc_infra/obs/metrics/asgi.py +8 -7
- svc_infra/obs/metrics/base.py +13 -13
- svc_infra/obs/metrics/http.py +3 -3
- svc_infra/obs/metrics/sqlalchemy.py +14 -13
- svc_infra/obs/metrics.py +9 -8
- svc_infra/resilience/__init__.py +44 -0
- svc_infra/resilience/circuit_breaker.py +328 -0
- svc_infra/resilience/retry.py +289 -0
- svc_infra/security/__init__.py +167 -0
- svc_infra/security/add.py +213 -0
- svc_infra/security/audit.py +97 -18
- svc_infra/security/audit_service.py +10 -9
- svc_infra/security/headers.py +15 -2
- svc_infra/security/hibp.py +14 -7
- svc_infra/security/jwt_rotation.py +78 -29
- svc_infra/security/lockout.py +23 -16
- svc_infra/security/models.py +77 -44
- svc_infra/security/oauth_models.py +73 -0
- svc_infra/security/org_invites.py +12 -12
- svc_infra/security/passwords.py +3 -3
- svc_infra/security/permissions.py +31 -7
- svc_infra/security/session.py +7 -8
- svc_infra/security/signed_cookies.py +26 -6
- svc_infra/storage/__init__.py +93 -0
- svc_infra/storage/add.py +250 -0
- svc_infra/storage/backends/__init__.py +11 -0
- svc_infra/storage/backends/local.py +331 -0
- svc_infra/storage/backends/memory.py +213 -0
- svc_infra/storage/backends/s3.py +334 -0
- svc_infra/storage/base.py +239 -0
- svc_infra/storage/easy.py +181 -0
- svc_infra/storage/settings.py +193 -0
- svc_infra/testing/__init__.py +682 -0
- svc_infra/utils.py +170 -5
- svc_infra/webhooks/__init__.py +69 -0
- svc_infra/webhooks/add.py +327 -0
- svc_infra/webhooks/encryption.py +115 -0
- svc_infra/webhooks/fastapi.py +37 -0
- svc_infra/webhooks/router.py +55 -0
- svc_infra/webhooks/service.py +69 -0
- svc_infra/webhooks/signing.py +34 -0
- svc_infra/websocket/__init__.py +79 -0
- svc_infra/websocket/add.py +139 -0
- svc_infra/websocket/client.py +283 -0
- svc_infra/websocket/config.py +57 -0
- svc_infra/websocket/easy.py +76 -0
- svc_infra/websocket/exceptions.py +61 -0
- svc_infra/websocket/manager.py +343 -0
- svc_infra/websocket/models.py +49 -0
- svc_infra-1.1.0.dist-info/LICENSE +21 -0
- svc_infra-1.1.0.dist-info/METADATA +362 -0
- svc_infra-1.1.0.dist-info/RECORD +364 -0
- svc_infra-0.1.595.dist-info/METADATA +0 -80
- svc_infra-0.1.595.dist-info/RECORD +0 -253
- {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/WHEEL +0 -0
- {svc_infra-0.1.595.dist-info → svc_infra-1.1.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Literal, cast
|
|
5
6
|
|
|
6
7
|
from fastapi import Body, Depends, Header, HTTPException, Request, Response, status
|
|
7
8
|
from starlette.responses import JSONResponse
|
|
@@ -50,7 +51,12 @@ from svc_infra.apf_payments.schemas import (
|
|
|
50
51
|
from svc_infra.apf_payments.service import PaymentsService
|
|
51
52
|
from svc_infra.api.fastapi.auth.security import OptionalIdentity, Principal
|
|
52
53
|
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
53
|
-
from svc_infra.api.fastapi.dual import
|
|
54
|
+
from svc_infra.api.fastapi.dual import (
|
|
55
|
+
protected_router,
|
|
56
|
+
public_router,
|
|
57
|
+
service_router,
|
|
58
|
+
user_router,
|
|
59
|
+
)
|
|
54
60
|
from svc_infra.api.fastapi.dual.router import DualAPIRouter
|
|
55
61
|
from svc_infra.api.fastapi.middleware.idempotency import require_idempotency_key
|
|
56
62
|
from svc_infra.api.fastapi.pagination import (
|
|
@@ -67,73 +73,83 @@ _TX_KINDS = {"payment", "refund", "fee", "payout", "capture"}
|
|
|
67
73
|
def _tx_kind(kind: str) -> Literal["payment", "refund", "fee", "payout", "capture"]:
|
|
68
74
|
if kind not in _TX_KINDS:
|
|
69
75
|
raise ValueError(f"Unknown ledger kind: {kind!r}")
|
|
70
|
-
return cast(Literal[
|
|
71
|
-
|
|
76
|
+
return cast("Literal['payment', 'refund', 'fee', 'payout', 'capture']", kind)
|
|
72
77
|
|
|
73
|
-
# --- deps ---
|
|
74
|
-
TenantOverrideHook = Callable[
|
|
75
|
-
[Request, Optional[Principal], Optional[str]],
|
|
76
|
-
Awaitable[Optional[str]] | Optional[str],
|
|
77
|
-
]
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
# --- tenant resolution ---
|
|
80
|
+
_tenant_resolver: Callable | None = None
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
def set_payments_tenant_resolver(
|
|
83
|
-
"""
|
|
83
|
+
def set_payments_tenant_resolver(fn):
|
|
84
|
+
"""Set or clear an override hook for payments tenant resolution.
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
fn(request: Request, identity: Principal | None, header: str | None) -> str | None
|
|
87
|
+
Return a tenant_id to override, or None to defer to default flow.
|
|
87
88
|
"""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
_tenant_override_hook = resolver
|
|
89
|
+
global _tenant_resolver
|
|
90
|
+
_tenant_resolver = fn
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
async def resolve_payments_tenant_id(
|
|
94
94
|
request: Request,
|
|
95
|
-
identity:
|
|
96
|
-
tenant_header:
|
|
95
|
+
identity: Principal | None = None,
|
|
96
|
+
tenant_header: str | None = None,
|
|
97
97
|
) -> str:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if identity:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
user_tenant = getattr(getattr(identity, "user", None), "tenant_id", None)
|
|
119
|
-
if user_tenant:
|
|
120
|
-
return user_tenant
|
|
121
|
-
|
|
98
|
+
# 1) Override hook
|
|
99
|
+
if _tenant_resolver is not None:
|
|
100
|
+
val = _tenant_resolver(request, identity, tenant_header)
|
|
101
|
+
# Support async or sync resolver
|
|
102
|
+
if inspect.isawaitable(val):
|
|
103
|
+
val = await val
|
|
104
|
+
if val:
|
|
105
|
+
return cast("str", val)
|
|
106
|
+
# if None, continue default flow
|
|
107
|
+
|
|
108
|
+
# 2) Principal (user)
|
|
109
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
110
|
+
return cast("str", identity.user.tenant_id)
|
|
111
|
+
|
|
112
|
+
# 3) Principal (api key)
|
|
113
|
+
if identity and getattr(identity.api_key or object(), "tenant_id", None):
|
|
114
|
+
return cast("str", identity.api_key.tenant_id)
|
|
115
|
+
|
|
116
|
+
# 4) Explicit header argument (tests pass this)
|
|
122
117
|
if tenant_header:
|
|
123
118
|
return tenant_header
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="tenant_context_missing")
|
|
130
|
-
|
|
120
|
+
# 5) Request state
|
|
121
|
+
state_tid = getattr(getattr(request, "state", object()), "tenant_id", None)
|
|
122
|
+
if state_tid:
|
|
123
|
+
return cast("str", state_tid)
|
|
131
124
|
|
|
132
|
-
|
|
125
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
133
126
|
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
# --- deps ---
|
|
129
|
+
async def get_service(
|
|
130
|
+
session: SqlSessionDep,
|
|
131
|
+
request: Request = ..., # type: ignore[assignment] # FastAPI will inject; tests may omit
|
|
132
|
+
identity: OptionalIdentity = None,
|
|
133
|
+
tenant_id: str | None = None,
|
|
134
|
+
) -> PaymentsService:
|
|
135
|
+
# Derive tenant id if not supplied explicitly
|
|
136
|
+
tid = tenant_id
|
|
137
|
+
if tid is None:
|
|
138
|
+
try:
|
|
139
|
+
if request is not ...:
|
|
140
|
+
tid = await resolve_payments_tenant_id(request, identity=identity)
|
|
141
|
+
else:
|
|
142
|
+
# allow tests to call without a Request; try identity or fallback
|
|
143
|
+
if identity and getattr(identity.user or object(), "tenant_id", None):
|
|
144
|
+
tid = identity.user.tenant_id
|
|
145
|
+
elif identity and getattr(identity.api_key or object(), "tenant_id", None):
|
|
146
|
+
tid = identity.api_key.tenant_id
|
|
147
|
+
else:
|
|
148
|
+
raise HTTPException(status_code=400, detail="tenant_context_missing")
|
|
149
|
+
except HTTPException:
|
|
150
|
+
# fallback for routes/tests that don't set context; preserve prior default
|
|
151
|
+
tid = "test_tenant"
|
|
152
|
+
return PaymentsService(session=session, tenant_id=tid)
|
|
137
153
|
|
|
138
154
|
|
|
139
155
|
# --- routers grouped by auth posture (same prefix is fine; FastAPI merges) ---
|
|
@@ -215,7 +231,9 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
215
231
|
tags=["Payment Intents", "Refunds"],
|
|
216
232
|
)
|
|
217
233
|
async def refund_intent(
|
|
218
|
-
provider_intent_id: str,
|
|
234
|
+
provider_intent_id: str,
|
|
235
|
+
data: RefundIn,
|
|
236
|
+
svc: PaymentsService = Depends(get_service),
|
|
219
237
|
):
|
|
220
238
|
out = await svc.refund(provider_intent_id, data)
|
|
221
239
|
await svc.session.flush()
|
|
@@ -273,7 +291,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
273
291
|
provider: str,
|
|
274
292
|
request: Request,
|
|
275
293
|
svc: PaymentsService = Depends(get_service),
|
|
276
|
-
signature:
|
|
294
|
+
signature: str | None = Header(None, alias="Stripe-Signature"),
|
|
277
295
|
):
|
|
278
296
|
payload = await request.body()
|
|
279
297
|
out = await svc.handle_webhook(provider.lower(), signature, payload)
|
|
@@ -535,8 +553,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
535
553
|
tags=["Payment Intents"],
|
|
536
554
|
)
|
|
537
555
|
async def list_intents_endpoint(
|
|
538
|
-
customer_provider_id:
|
|
539
|
-
status:
|
|
556
|
+
customer_provider_id: str | None = None,
|
|
557
|
+
status: str | None = None,
|
|
540
558
|
svc: PaymentsService = Depends(get_service),
|
|
541
559
|
):
|
|
542
560
|
ctx = use_pagination()
|
|
@@ -576,8 +594,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
576
594
|
tags=["Invoices"],
|
|
577
595
|
)
|
|
578
596
|
async def list_invoices_endpoint(
|
|
579
|
-
customer_provider_id:
|
|
580
|
-
status:
|
|
597
|
+
customer_provider_id: str | None = None,
|
|
598
|
+
status: str | None = None,
|
|
581
599
|
svc: PaymentsService = Depends(get_service),
|
|
582
600
|
):
|
|
583
601
|
ctx = use_pagination()
|
|
@@ -611,7 +629,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
611
629
|
)
|
|
612
630
|
async def preview_invoice_endpoint(
|
|
613
631
|
customer_provider_id: str,
|
|
614
|
-
subscription_id:
|
|
632
|
+
subscription_id: str | None = None,
|
|
615
633
|
svc: PaymentsService = Depends(get_service),
|
|
616
634
|
):
|
|
617
635
|
return await svc.preview_invoice(customer_provider_id, subscription_id)
|
|
@@ -699,7 +717,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
699
717
|
tags=["Disputes"],
|
|
700
718
|
)
|
|
701
719
|
async def list_disputes(
|
|
702
|
-
status:
|
|
720
|
+
status: str | None = None,
|
|
703
721
|
svc: PaymentsService = Depends(get_service),
|
|
704
722
|
):
|
|
705
723
|
ctx = use_pagination()
|
|
@@ -735,7 +753,10 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
735
753
|
|
|
736
754
|
# ===== Balance & Payouts =====
|
|
737
755
|
@prot.get(
|
|
738
|
-
"/balance",
|
|
756
|
+
"/balance",
|
|
757
|
+
name="payments_get_balance",
|
|
758
|
+
response_model=BalanceSnapshotOut,
|
|
759
|
+
tags=["Balance"],
|
|
739
760
|
)
|
|
740
761
|
async def get_balance(svc: PaymentsService = Depends(get_service)):
|
|
741
762
|
return await svc.get_balance_snapshot()
|
|
@@ -770,8 +791,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
770
791
|
tags=["Webhooks"],
|
|
771
792
|
)
|
|
772
793
|
async def replay_webhooks(
|
|
773
|
-
since:
|
|
774
|
-
until:
|
|
794
|
+
since: str | None = None,
|
|
795
|
+
until: str | None = None,
|
|
775
796
|
data: WebhookReplayIn = Body(default=WebhookReplayIn()),
|
|
776
797
|
svc: PaymentsService = Depends(get_service),
|
|
777
798
|
):
|
|
@@ -788,8 +809,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
788
809
|
tags=["Customers"],
|
|
789
810
|
)
|
|
790
811
|
async def list_customers_endpoint(
|
|
791
|
-
provider:
|
|
792
|
-
user_id:
|
|
812
|
+
provider: str | None = None,
|
|
813
|
+
user_id: str | None = None,
|
|
793
814
|
svc: PaymentsService = Depends(get_service),
|
|
794
815
|
):
|
|
795
816
|
ctx = use_pagination()
|
|
@@ -857,7 +878,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
857
878
|
tags=["Products"],
|
|
858
879
|
)
|
|
859
880
|
async def list_products_endpoint(
|
|
860
|
-
active:
|
|
881
|
+
active: bool | None = None,
|
|
861
882
|
svc: PaymentsService = Depends(get_service),
|
|
862
883
|
):
|
|
863
884
|
ctx = use_pagination()
|
|
@@ -902,8 +923,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
902
923
|
tags=["Prices"],
|
|
903
924
|
)
|
|
904
925
|
async def list_prices_endpoint(
|
|
905
|
-
provider_product_id:
|
|
906
|
-
active:
|
|
926
|
+
provider_product_id: str | None = None,
|
|
927
|
+
active: bool | None = None,
|
|
907
928
|
svc: PaymentsService = Depends(get_service),
|
|
908
929
|
):
|
|
909
930
|
ctx = use_pagination()
|
|
@@ -951,8 +972,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
951
972
|
tags=["Subscriptions"],
|
|
952
973
|
)
|
|
953
974
|
async def list_subscriptions_endpoint(
|
|
954
|
-
customer_provider_id:
|
|
955
|
-
status:
|
|
975
|
+
customer_provider_id: str | None = None,
|
|
976
|
+
status: str | None = None,
|
|
956
977
|
svc: PaymentsService = Depends(get_service),
|
|
957
978
|
):
|
|
958
979
|
ctx = use_pagination()
|
|
@@ -991,7 +1012,7 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
991
1012
|
tags=["Refunds"],
|
|
992
1013
|
)
|
|
993
1014
|
async def list_refunds_endpoint(
|
|
994
|
-
provider_payment_intent_id:
|
|
1015
|
+
provider_payment_intent_id: str | None = None,
|
|
995
1016
|
svc: PaymentsService = Depends(get_service),
|
|
996
1017
|
):
|
|
997
1018
|
ctx = use_pagination()
|
|
@@ -1022,8 +1043,8 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
|
|
|
1022
1043
|
tags=["Usage Records"],
|
|
1023
1044
|
)
|
|
1024
1045
|
async def list_usage_records_endpoint(
|
|
1025
|
-
subscription_item:
|
|
1026
|
-
provider_price_id:
|
|
1046
|
+
subscription_item: str | None = None,
|
|
1047
|
+
provider_price_id: str | None = None,
|
|
1027
1048
|
svc: PaymentsService = Depends(get_service),
|
|
1028
1049
|
):
|
|
1029
1050
|
ctx = use_pagination()
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
5
6
|
|
|
6
7
|
from fastapi import FastAPI
|
|
7
8
|
|
|
8
9
|
from svc_infra.apf_payments.provider.registry import get_provider_registry
|
|
9
10
|
from svc_infra.api.fastapi.apf_payments.router import build_payments_routers
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from svc_infra.apf_payments.provider.base import ProviderAdapter
|
|
11
14
|
|
|
12
15
|
logger = logging.getLogger(__name__)
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
def _maybe_register_default_providers(
|
|
16
|
-
register_defaults: bool, adapters: Optional[Iterable[object]]
|
|
17
|
-
):
|
|
18
|
+
def _maybe_register_default_providers(register_defaults: bool, adapters: Iterable[object] | None):
|
|
18
19
|
reg = get_provider_registry()
|
|
19
20
|
if register_defaults:
|
|
20
21
|
# Try Stripe by default; silently skip if not configured
|
|
@@ -32,7 +33,7 @@ def _maybe_register_default_providers(
|
|
|
32
33
|
pass
|
|
33
34
|
if adapters:
|
|
34
35
|
for a in adapters:
|
|
35
|
-
reg.register(a) # must implement ProviderAdapter protocol
|
|
36
|
+
reg.register(cast("ProviderAdapter", a)) # must implement ProviderAdapter protocol
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
def add_payments(
|
|
@@ -40,7 +41,7 @@ def add_payments(
|
|
|
40
41
|
*,
|
|
41
42
|
prefix: str = "/payments",
|
|
42
43
|
register_default_providers: bool = True,
|
|
43
|
-
adapters:
|
|
44
|
+
adapters: Iterable[object] | None = None,
|
|
44
45
|
include_in_docs: bool | None = None, # None = keep your env-based default visibility
|
|
45
46
|
) -> None:
|
|
46
47
|
"""
|
|
@@ -51,11 +52,11 @@ def add_payments(
|
|
|
51
52
|
- Reuses your OpenAPI defaults (security + responses) via DualAPIRouter factories.
|
|
52
53
|
"""
|
|
53
54
|
_maybe_register_default_providers(register_default_providers, adapters)
|
|
54
|
-
add_prefixed_docs(app, prefix=prefix, title="Payments")
|
|
55
55
|
|
|
56
56
|
for r in build_payments_routers(prefix=prefix):
|
|
57
57
|
app.include_router(
|
|
58
|
-
r,
|
|
58
|
+
r,
|
|
59
|
+
include_in_schema=True if include_in_docs is None else bool(include_in_docs),
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
# Store the startup function to be called by lifespan if needed
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Authentication module for svc-infra.
|
|
2
|
+
|
|
3
|
+
Provides user authentication, authorization, and security primitives.
|
|
4
|
+
|
|
5
|
+
Key exports:
|
|
6
|
+
- add_auth_users: Add authentication routes to FastAPI app
|
|
7
|
+
- Identity, OptionalIdentity: Annotated dependencies for auth
|
|
8
|
+
- RequireUser, RequireRoles, RequireScopes: Authorization guards
|
|
9
|
+
- Principal: Unified identity (user via JWT/cookie or service via API key)
|
|
10
|
+
- AuthSettings, get_auth_settings: Auth configuration
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
# These imports are safe (no circular dependency)
|
|
18
|
+
from .policy import AuthPolicy, DefaultAuthPolicy
|
|
19
|
+
from .security import (
|
|
20
|
+
Identity,
|
|
21
|
+
OptionalIdentity,
|
|
22
|
+
Principal,
|
|
23
|
+
RequireAnyScope,
|
|
24
|
+
RequireIdentity,
|
|
25
|
+
RequireRoles,
|
|
26
|
+
RequireScopes,
|
|
27
|
+
RequireService,
|
|
28
|
+
RequireUser,
|
|
29
|
+
)
|
|
30
|
+
from .settings import AuthSettings, JWTSettings, OIDCProvider, get_auth_settings
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .add import add_auth_users as add_auth_users
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Main setup
|
|
37
|
+
"add_auth_users",
|
|
38
|
+
# Identity/Auth guards
|
|
39
|
+
"Identity",
|
|
40
|
+
"OptionalIdentity",
|
|
41
|
+
"Principal",
|
|
42
|
+
"RequireIdentity",
|
|
43
|
+
"RequireUser",
|
|
44
|
+
"RequireService",
|
|
45
|
+
"RequireRoles",
|
|
46
|
+
"RequireScopes",
|
|
47
|
+
"RequireAnyScope",
|
|
48
|
+
# Policy
|
|
49
|
+
"AuthPolicy",
|
|
50
|
+
"DefaultAuthPolicy",
|
|
51
|
+
# Settings
|
|
52
|
+
"AuthSettings",
|
|
53
|
+
"get_auth_settings",
|
|
54
|
+
"JWTSettings",
|
|
55
|
+
"OIDCProvider",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def __getattr__(name: str):
|
|
60
|
+
"""Lazy import for add_auth_users to avoid circular import."""
|
|
61
|
+
if name == "add_auth_users":
|
|
62
|
+
from .add import add_auth_users
|
|
63
|
+
|
|
64
|
+
return add_auth_users
|
|
65
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Optional
|
|
4
|
-
|
|
5
3
|
from starlette.requests import Request
|
|
6
4
|
|
|
7
5
|
from svc_infra.app.env import IS_PROD
|
|
@@ -25,7 +23,7 @@ def compute_cookie_params(request: Request, *, name: str) -> dict:
|
|
|
25
23
|
st = get_auth_settings()
|
|
26
24
|
cfg_domain = (getattr(st, "session_cookie_domain", "") or "").strip()
|
|
27
25
|
|
|
28
|
-
domain:
|
|
26
|
+
domain: str | None = None
|
|
29
27
|
if cfg_domain and not _is_local_host(cfg_domain):
|
|
30
28
|
domain = cfg_domain
|
|
31
29
|
|
|
@@ -14,10 +14,9 @@ from svc_infra.api.fastapi.auth.routers.oauth_router import oauth_router_with_ba
|
|
|
14
14
|
from svc_infra.api.fastapi.auth.routers.session_router import build_session_router
|
|
15
15
|
from svc_infra.api.fastapi.db.sql.users import get_fastapi_users
|
|
16
16
|
from svc_infra.api.fastapi.paths.prefix import AUTH_PREFIX, USER_PREFIX
|
|
17
|
-
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV
|
|
17
|
+
from svc_infra.app.env import CURRENT_ENVIRONMENT, DEV_ENV, LOCAL_ENV, require_secret
|
|
18
18
|
from svc_infra.db.sql.apikey import bind_apikey_model
|
|
19
19
|
|
|
20
|
-
from ..docs.scoped import add_prefixed_docs
|
|
21
20
|
from .policy import AuthPolicy, DefaultAuthPolicy
|
|
22
21
|
from .providers import providers_from_settings
|
|
23
22
|
from .settings import get_auth_settings
|
|
@@ -136,12 +135,12 @@ def setup_oauth_authentication(
|
|
|
136
135
|
if not providers:
|
|
137
136
|
return
|
|
138
137
|
|
|
138
|
+
redirect_url = post_login_redirect or getattr(settings_obj, "post_login_redirect", None) or "/"
|
|
139
139
|
oauth_router_instance = oauth_router_with_backend(
|
|
140
140
|
user_model=user_model,
|
|
141
141
|
auth_backend=auth_backend,
|
|
142
142
|
providers=providers,
|
|
143
|
-
post_login_redirect=
|
|
144
|
-
or getattr(settings_obj, "post_login_redirect", "/"),
|
|
143
|
+
post_login_redirect=redirect_url,
|
|
145
144
|
provider_account_model=provider_account_model,
|
|
146
145
|
auth_policy=auth_policy,
|
|
147
146
|
)
|
|
@@ -252,7 +251,7 @@ def add_auth_users(
|
|
|
252
251
|
(
|
|
253
252
|
fapi,
|
|
254
253
|
auth_backend,
|
|
255
|
-
|
|
254
|
+
_auth_router,
|
|
256
255
|
users_router,
|
|
257
256
|
get_strategy,
|
|
258
257
|
register_router,
|
|
@@ -273,15 +272,18 @@ def add_auth_users(
|
|
|
273
272
|
policy = auth_policy or DefaultAuthPolicy(settings_obj)
|
|
274
273
|
include_in_docs = CURRENT_ENVIRONMENT in (LOCAL_ENV, DEV_ENV)
|
|
275
274
|
|
|
276
|
-
if not any(m.cls.__name__ == "SessionMiddleware" for m in app.user_middleware):
|
|
275
|
+
if not any(m.cls.__name__ == "SessionMiddleware" for m in app.user_middleware): # type: ignore[attr-defined]
|
|
277
276
|
jwt_block = getattr(settings_obj, "jwt", None)
|
|
278
|
-
|
|
279
|
-
jwt_block.secret.get_secret_value()
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
if jwt_block and getattr(jwt_block, "secret", None):
|
|
278
|
+
secret = jwt_block.secret.get_secret_value()
|
|
279
|
+
else:
|
|
280
|
+
secret = require_secret(
|
|
281
|
+
None,
|
|
282
|
+
"JWT_SECRET (via auth settings jwt.secret for SessionMiddleware)",
|
|
283
|
+
dev_default="dev-only-session-jwt-secret-not-for-production",
|
|
284
|
+
)
|
|
283
285
|
same_site_lit = cast(
|
|
284
|
-
Literal[
|
|
286
|
+
"Literal['lax', 'strict', 'none']",
|
|
285
287
|
str(getattr(settings_obj, "session_cookie_samesite", "lax")).lower(),
|
|
286
288
|
)
|
|
287
289
|
app.add_middleware(
|
|
@@ -293,9 +295,6 @@ def add_auth_users(
|
|
|
293
295
|
https_only=bool(getattr(settings_obj, "session_cookie_secure", False)),
|
|
294
296
|
)
|
|
295
297
|
|
|
296
|
-
add_prefixed_docs(app, prefix=user_prefix, title="Users")
|
|
297
|
-
add_prefixed_docs(app, prefix=auth_prefix, title="Auth")
|
|
298
|
-
|
|
299
298
|
if enable_password:
|
|
300
299
|
setup_password_authentication(
|
|
301
300
|
app,
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
|
|
7
8
|
from fastapi.responses import JSONResponse
|
|
8
9
|
from fastapi_users import FastAPIUsers
|
|
9
|
-
from fastapi_users.authentication import AuthenticationBackend
|
|
10
|
+
from fastapi_users.authentication import AuthenticationBackend, Strategy
|
|
10
11
|
from fastapi_users.password import PasswordHelper
|
|
12
|
+
from starlette.datastructures import FormData
|
|
11
13
|
|
|
12
14
|
from svc_infra.api.fastapi.auth._cookies import compute_cookie_params
|
|
13
15
|
from svc_infra.api.fastapi.auth.policy import AuthPolicy, DefaultAuthPolicy
|
|
14
16
|
from svc_infra.api.fastapi.auth.settings import get_auth_settings
|
|
17
|
+
from svc_infra.api.fastapi.db.sql.session import SqlSessionDep
|
|
15
18
|
from svc_infra.api.fastapi.dual.public import public_router
|
|
16
19
|
|
|
17
20
|
_pwd = PasswordHelper()
|
|
@@ -30,16 +33,20 @@ async def login_client_gaurd(request: Request):
|
|
|
30
33
|
|
|
31
34
|
# only enforce on the login endpoint (form-encoded)
|
|
32
35
|
if request.method.upper() == "POST" and request.url.path.endswith("/login"):
|
|
36
|
+
form: FormData | dict[str, Any]
|
|
33
37
|
try:
|
|
34
38
|
form = await request.form()
|
|
35
39
|
except Exception:
|
|
36
40
|
form = {}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
client_id_raw = form.get("client_id")
|
|
43
|
+
client_secret_raw = form.get("client_secret")
|
|
44
|
+
client_id = client_id_raw.strip() if isinstance(client_id_raw, str) else ""
|
|
45
|
+
client_secret = client_secret_raw.strip() if isinstance(client_secret_raw, str) else ""
|
|
40
46
|
if not client_id or not client_secret:
|
|
41
47
|
raise HTTPException(
|
|
42
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
48
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
49
|
+
detail="client_credentials_required",
|
|
43
50
|
)
|
|
44
51
|
|
|
45
52
|
# validate against configured clients
|
|
@@ -51,7 +58,8 @@ async def login_client_gaurd(request: Request):
|
|
|
51
58
|
|
|
52
59
|
if not ok:
|
|
53
60
|
raise HTTPException(
|
|
54
|
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
61
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
62
|
+
detail="invalid_client_credentials",
|
|
55
63
|
)
|
|
56
64
|
|
|
57
65
|
|
|
@@ -66,21 +74,20 @@ def auth_session_router(
|
|
|
66
74
|
router = public_router()
|
|
67
75
|
policy = auth_policy or DefaultAuthPolicy(get_auth_settings())
|
|
68
76
|
|
|
69
|
-
from svc_infra.api.fastapi.db.sql import SqlSessionDep
|
|
70
77
|
from svc_infra.security.lockout import get_lockout_status, record_attempt
|
|
71
78
|
|
|
72
79
|
@router.post("/login", name="auth:jwt.login")
|
|
73
80
|
async def login(
|
|
74
81
|
request: Request,
|
|
82
|
+
session: SqlSessionDep,
|
|
75
83
|
username: str = Form(...),
|
|
76
84
|
password: str = Form(...),
|
|
77
85
|
scope: str = Form(""),
|
|
78
86
|
client_id: str | None = Form(None),
|
|
79
87
|
client_secret: str | None = Form(None),
|
|
88
|
+
strategy: Strategy[Any, Any] = Depends(auth_backend.get_strategy),
|
|
80
89
|
user_manager=Depends(fapi.get_user_manager),
|
|
81
|
-
session: SqlSessionDep = Depends(),
|
|
82
90
|
):
|
|
83
|
-
strategy = auth_backend.get_strategy()
|
|
84
91
|
email = username.strip().lower()
|
|
85
92
|
# Compute IP hash for lockout correlation
|
|
86
93
|
client_ip = getattr(request.client, "host", None)
|
|
@@ -90,9 +97,7 @@ def auth_session_router(
|
|
|
90
97
|
try:
|
|
91
98
|
status_lo = await get_lockout_status(session, user_id=None, ip_hash=ip_hash)
|
|
92
99
|
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
|
-
)
|
|
100
|
+
retry = int((status_lo.next_allowed_at - datetime.now(UTC)).total_seconds())
|
|
96
101
|
raise HTTPException(
|
|
97
102
|
status_code=429,
|
|
98
103
|
detail="account_locked",
|
|
@@ -119,7 +124,10 @@ def auth_session_router(
|
|
|
119
124
|
if not hashed:
|
|
120
125
|
try:
|
|
121
126
|
await record_attempt(
|
|
122
|
-
session,
|
|
127
|
+
session,
|
|
128
|
+
user_id=getattr(user, "id", None),
|
|
129
|
+
ip_hash=ip_hash,
|
|
130
|
+
success=False,
|
|
123
131
|
)
|
|
124
132
|
except Exception:
|
|
125
133
|
pass
|
|
@@ -131,9 +139,7 @@ def auth_session_router(
|
|
|
131
139
|
session, user_id=getattr(user, "id", None), ip_hash=ip_hash
|
|
132
140
|
)
|
|
133
141
|
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
|
-
)
|
|
142
|
+
retry = int((status_user.next_allowed_at - datetime.now(UTC)).total_seconds())
|
|
137
143
|
raise HTTPException(
|
|
138
144
|
status_code=429,
|
|
139
145
|
detail="account_locked",
|
|
@@ -146,7 +152,10 @@ def auth_session_router(
|
|
|
146
152
|
if not ok:
|
|
147
153
|
try:
|
|
148
154
|
await record_attempt(
|
|
149
|
-
session,
|
|
155
|
+
session,
|
|
156
|
+
user_id=getattr(user, "id", None),
|
|
157
|
+
ip_hash=ip_hash,
|
|
158
|
+
success=False,
|
|
150
159
|
)
|
|
151
160
|
except Exception:
|
|
152
161
|
pass
|
|
@@ -163,7 +172,7 @@ def auth_session_router(
|
|
|
163
172
|
except Exception:
|
|
164
173
|
pass
|
|
165
174
|
|
|
166
|
-
if
|
|
175
|
+
if user.is_verified is False:
|
|
167
176
|
raise HTTPException(400, "LOGIN_USER_NOT_VERIFIED")
|
|
168
177
|
|
|
169
178
|
# 3) MFA policy check (user flag, tenant/global, etc.)
|
|
@@ -178,7 +187,7 @@ def auth_session_router(
|
|
|
178
187
|
|
|
179
188
|
# 4) record last_login for password logins that do NOT require MFA
|
|
180
189
|
try:
|
|
181
|
-
user.last_login = datetime.now(
|
|
190
|
+
user.last_login = datetime.now(UTC)
|
|
182
191
|
await user_manager.user_db.update(user, {"last_login": user.last_login})
|
|
183
192
|
except Exception:
|
|
184
193
|
# don’t block login if this write fails
|
|
@@ -187,7 +196,10 @@ def auth_session_router(
|
|
|
187
196
|
# Record successful attempt (for audit)
|
|
188
197
|
try:
|
|
189
198
|
await record_attempt(
|
|
190
|
-
session,
|
|
199
|
+
session,
|
|
200
|
+
user_id=getattr(user, "id", None),
|
|
201
|
+
ip_hash=ip_hash,
|
|
202
|
+
success=True,
|
|
191
203
|
)
|
|
192
204
|
except Exception:
|
|
193
205
|
pass
|