Поправили заполлнение БД + поправили гонку сообщений при отправке в Битрикс
This commit is contained in:
@@ -40,6 +40,7 @@ from app.db import (
|
||||
PopularQuestion,
|
||||
TextResource,
|
||||
UserIdentity,
|
||||
UxSession,
|
||||
)
|
||||
from app.integrations import (
|
||||
DependencyFailure,
|
||||
@@ -62,6 +63,7 @@ from app.schemas import (
|
||||
encode_cursor,
|
||||
)
|
||||
from app.services import (
|
||||
AuditContext,
|
||||
DomainError,
|
||||
SettingsSnapshot,
|
||||
apply_inbox,
|
||||
@@ -153,7 +155,7 @@ app = FastAPI(
|
||||
log = structlog.get_logger()
|
||||
|
||||
|
||||
def client_ip(request: Request) -> str:
|
||||
def client_ip(request: Request) -> str | None:
|
||||
"""Trust forwarded client addresses only from configured reverse proxies."""
|
||||
peer = request.client.host if request.client else ""
|
||||
try:
|
||||
@@ -170,7 +172,40 @@ def client_ip(request: Request) -> str:
|
||||
try:
|
||||
return str(ip_address(candidate))
|
||||
except ValueError:
|
||||
return "unknown"
|
||||
return None
|
||||
|
||||
|
||||
def request_trace_id(request: Request) -> str:
|
||||
parts = request.headers.get("traceparent", "").lower().split("-")
|
||||
if (
|
||||
len(parts) == 4
|
||||
and len(parts[0]) == 2
|
||||
and len(parts[1]) == 32
|
||||
and len(parts[2]) == 16
|
||||
and len(parts[3]) == 2
|
||||
):
|
||||
try:
|
||||
int("".join(parts), 16)
|
||||
if parts[1] != "0" * 32 and parts[2] != "0" * 16:
|
||||
return parts[1]
|
||||
except ValueError:
|
||||
pass
|
||||
return uuid.uuid4().hex
|
||||
|
||||
|
||||
def user_agent_hash(request: Request) -> str | None:
|
||||
value = " ".join(request.headers.get("User-Agent", "").split())
|
||||
return hashlib.sha256(value[:1024].encode()).hexdigest() if value else None
|
||||
|
||||
|
||||
def audit_context(request: Request, ux_session_id: uuid.UUID | None = None) -> AuditContext:
|
||||
return AuditContext(
|
||||
request_id=request.state.request_id,
|
||||
trace_id=request.state.trace_id,
|
||||
ux_session_id=ux_session_id,
|
||||
user_agent_hash=request.state.user_agent_hash,
|
||||
client_ip=client_ip(request),
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
@@ -181,10 +216,13 @@ async def request_context(request: Request, call_next: Any) -> Response:
|
||||
except ValueError:
|
||||
request_id = str(uuid.uuid4())
|
||||
request.state.request_id = request_id
|
||||
request.state.trace_id = request_trace_id(request)
|
||||
request.state.user_agent_hash = user_agent_hash(request)
|
||||
request.state.started_at = time.monotonic()
|
||||
structlog.contextvars.clear_contextvars()
|
||||
structlog.contextvars.bind_contextvars(
|
||||
request_id=request_id,
|
||||
trace_id=request.state.trace_id,
|
||||
ux_session_id=request.headers.get("X-Ux-Session-Id"),
|
||||
method=request.method,
|
||||
route=request.url.path,
|
||||
@@ -291,6 +329,26 @@ async def current_user(db: Session, auth: PrincipalDep) -> UserIdentity:
|
||||
UserDep = Annotated[UserIdentity, Depends(current_user)]
|
||||
|
||||
|
||||
async def required_user_audit_context(
|
||||
request: Request,
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
x_ux_session_id: Annotated[str, Header(alias="X-Ux-Session-Id")],
|
||||
) -> AuditContext:
|
||||
session_id = ux_id(x_ux_session_id)
|
||||
if session_id is None:
|
||||
raise DomainError("validation_error", 400, "X-Ux-Session-Id is required")
|
||||
exists = await db.scalar(
|
||||
select(UxSession.id).where(UxSession.id == session_id, UxSession.user_id == user.id)
|
||||
)
|
||||
if exists is None:
|
||||
raise DomainError("validation_error", 400, "X-Ux-Session-Id is invalid")
|
||||
return audit_context(request, session_id)
|
||||
|
||||
|
||||
UserAuditContextDep = Annotated[AuditContext, Depends(required_user_audit_context)]
|
||||
|
||||
|
||||
async def snapshot(db: Session) -> SettingsSnapshot:
|
||||
return await load_settings(db)
|
||||
|
||||
@@ -357,7 +415,7 @@ async def ready(request: Request, db: Session):
|
||||
try:
|
||||
await db.execute(text("SELECT 1"))
|
||||
revision = await db.scalar(text("SELECT version_num FROM han_app.alembic_version LIMIT 1"))
|
||||
if revision != "0002_pgcrypto_digest":
|
||||
if revision != "0003_consent_audit":
|
||||
raise RuntimeError("unexpected database revision")
|
||||
await load_settings(db)
|
||||
components["postgres"] = "ok"
|
||||
@@ -400,7 +458,7 @@ async def app_config(request: Request, response: Response, settings: SnapshotDep
|
||||
await enforce_limit(
|
||||
request,
|
||||
"ip",
|
||||
client_ip(request),
|
||||
client_ip(request) or "unknown",
|
||||
"public",
|
||||
settings.limit("rate_limit.public_endpoints.per_ip"),
|
||||
fail_closed=False,
|
||||
@@ -444,7 +502,7 @@ async def content(
|
||||
await enforce_limit(
|
||||
request,
|
||||
"ip",
|
||||
client_ip(request),
|
||||
client_ip(request) or "unknown",
|
||||
"public",
|
||||
settings.limit("rate_limit.public_endpoints.per_ip"),
|
||||
fail_closed=False,
|
||||
@@ -480,7 +538,7 @@ async def content(
|
||||
async def auth_bootstrap(
|
||||
body: BootstrapRequest, request: Request, db: Session, auth: PrincipalDep, settings: SnapshotDep
|
||||
):
|
||||
return await bootstrap(db, auth, body, settings, request.state.request_id)
|
||||
return await bootstrap(db, auth, body, settings, audit_context(request))
|
||||
|
||||
|
||||
@app.post("/api/v1/consents", status_code=201, tags=["auth"])
|
||||
@@ -490,18 +548,16 @@ async def consents(
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
settings: SnapshotDep,
|
||||
x_ux_session_id: Annotated[str | None, Header()] = None,
|
||||
context: UserAuditContextDep,
|
||||
):
|
||||
return await record_consents(
|
||||
db, user, body, settings, request.state.request_id, ux_id(x_ux_session_id)
|
||||
)
|
||||
return await record_consents(db, user, body, settings, context)
|
||||
|
||||
|
||||
@app.post("/api/v1/analytics/session-start", status_code=201, tags=["analytics"])
|
||||
async def analytics_session(
|
||||
body: SessionStartRequest, request: Request, db: Session, user: UserDep
|
||||
):
|
||||
return await start_session(db, user, body, request.state.request_id)
|
||||
return await start_session(db, user, body, audit_context(request))
|
||||
|
||||
|
||||
@app.get("/api/v1/me", tags=["profile"])
|
||||
@@ -564,7 +620,7 @@ async def document_download(
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
settings: SnapshotDep,
|
||||
x_ux_session_id: Annotated[str | None, Header()] = None,
|
||||
context: UserAuditContextDep,
|
||||
):
|
||||
await enforce_limit(
|
||||
request,
|
||||
@@ -578,11 +634,11 @@ async def document_download(
|
||||
db.add(
|
||||
audit(
|
||||
"document.download_url_issued",
|
||||
request.state.request_id,
|
||||
context,
|
||||
user.id,
|
||||
"document",
|
||||
item.id,
|
||||
ux_id(x_ux_session_id),
|
||||
metadata={"expires_in_seconds": 300},
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -595,6 +651,7 @@ async def dialogs_create(
|
||||
request: Request,
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
context: UserAuditContextDep,
|
||||
idempotency_key: Annotated[str | None, Header()] = None,
|
||||
):
|
||||
key = await required_idempotency(idempotency_key)
|
||||
@@ -608,7 +665,7 @@ async def dialogs_create(
|
||||
if cached["fingerprint"] != fingerprint:
|
||||
raise DomainError("idempotency_key_reused", 409, "Idempotency key was reused")
|
||||
return JSONResponse(cached["body"], status_code=cached["status"])
|
||||
body, status = await create_dialog(db, user, request.state.request_id, key)
|
||||
body, status = await create_dialog(db, user, context, key)
|
||||
try:
|
||||
await request.app.state.idempotency.put(
|
||||
scope,
|
||||
@@ -735,6 +792,7 @@ async def message_create(
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
business: SnapshotDep,
|
||||
context: UserAuditContextDep,
|
||||
idempotency_key: Annotated[str | None, Header()] = None,
|
||||
):
|
||||
key = await required_idempotency(idempotency_key)
|
||||
@@ -772,7 +830,7 @@ async def message_create(
|
||||
dialog_id,
|
||||
body,
|
||||
key,
|
||||
request.state.request_id,
|
||||
context,
|
||||
request.app.state.settings,
|
||||
request.app.state.safety,
|
||||
request.app.state.openlines,
|
||||
@@ -803,6 +861,7 @@ async def attachment_init(
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
settings: SnapshotDep,
|
||||
context: UserAuditContextDep,
|
||||
):
|
||||
await enforce_limit(
|
||||
request,
|
||||
@@ -812,9 +871,7 @@ async def attachment_init(
|
||||
settings.limit("rate_limit.message_send.per_user"),
|
||||
fail_closed=True,
|
||||
)
|
||||
return await init_attachment(
|
||||
db, user, dialog_id, body, settings, request.app.state.s3, request.state.request_id
|
||||
)
|
||||
return await init_attachment(db, user, dialog_id, body, settings, request.app.state.s3, context)
|
||||
|
||||
|
||||
@app.post(
|
||||
@@ -828,9 +885,10 @@ async def attachment_complete(
|
||||
request: Request,
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
context: UserAuditContextDep,
|
||||
):
|
||||
return await complete_attachment(
|
||||
db, user, dialog_id, attachment_id, body, request.app.state.s3, request.state.request_id
|
||||
db, user, dialog_id, attachment_id, body, request.app.state.s3, context
|
||||
)
|
||||
|
||||
|
||||
@@ -845,7 +903,7 @@ async def attachment_download(
|
||||
db: Session,
|
||||
user: UserDep,
|
||||
settings: SnapshotDep,
|
||||
x_ux_session_id: Annotated[str | None, Header()] = None,
|
||||
context: UserAuditContextDep,
|
||||
):
|
||||
await enforce_limit(
|
||||
request,
|
||||
@@ -871,11 +929,11 @@ async def attachment_download(
|
||||
db.add(
|
||||
audit(
|
||||
"attachment.download_url_issued",
|
||||
request.state.request_id,
|
||||
context,
|
||||
user.id,
|
||||
"attachment",
|
||||
item.id,
|
||||
ux_id(x_ux_session_id),
|
||||
metadata={"expires_in_seconds": 300},
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@@ -895,7 +953,7 @@ async def inbox(event: OpenLinesInbox, request: Request, db: Session, settings:
|
||||
body, status = await apply_inbox(
|
||||
db,
|
||||
event,
|
||||
request.state.request_id,
|
||||
audit_context(request),
|
||||
settings,
|
||||
request.app.state.s3,
|
||||
request.app.state.http,
|
||||
|
||||
Reference in New Issue
Block a user