Реализованы задачи бэклога 1-5 (1я не до конца)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Store raw device identifiers and add OTP verification limit.
|
||||
|
||||
Revision ID: 0004_device_otp
|
||||
Revises: 0003_consent_audit
|
||||
Create Date: 2026-07-21
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0004_device_otp"
|
||||
down_revision: str | None = "0003_consent_audit"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"""
|
||||
ALTER TABLE han_app.ux_sessions
|
||||
ADD COLUMN IF NOT EXISTS device_id varchar(255)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO han_app.app_settings
|
||||
(setting_key, setting_value, value_type, is_public, description,
|
||||
record_status, updated_at)
|
||||
VALUES
|
||||
('otp.phone.max_verify_attempts', '5', 'integer', false,
|
||||
'Maximum failed verification attempts for one OTP challenge',
|
||||
'A', now())
|
||||
ON CONFLICT (setting_key) DO NOTHING
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
raise RuntimeError("Device and OTP limits migration is forward-only")
|
||||
@@ -103,6 +103,7 @@ class UxSession(Common, Base):
|
||||
platform: Mapped[str] = mapped_column(String(32))
|
||||
app_version: Mapped[str] = mapped_column(String(64))
|
||||
device_id_hash: Mapped[str | None] = mapped_column(String(64))
|
||||
device_id: Mapped[str | None] = mapped_column(String(255))
|
||||
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
|
||||
|
||||
@@ -415,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 != "0003_consent_audit":
|
||||
if revision != "0004_device_otp":
|
||||
raise RuntimeError("unexpected database revision")
|
||||
await load_settings(db)
|
||||
components["postgres"] = "ok"
|
||||
@@ -979,6 +979,7 @@ async def otp_settings(
|
||||
return JSONResponse({
|
||||
"max_send_attempts_per_24h": settings.integer("otp.phone.max_send_attempts_per_24h"),
|
||||
"min_seconds_between_attempts": settings.integer("otp.phone.min_seconds_between_attempts"),
|
||||
"max_verify_attempts": settings.integer("otp.phone.max_verify_attempts"),
|
||||
"version": settings.version,
|
||||
"cache_ttl_seconds": 60,
|
||||
}, headers=headers)
|
||||
|
||||
@@ -63,6 +63,7 @@ REQUIRED_SETTINGS = {
|
||||
"auth.password.enabled",
|
||||
"otp.phone.max_send_attempts_per_24h",
|
||||
"otp.phone.min_seconds_between_attempts",
|
||||
"otp.phone.max_verify_attempts",
|
||||
"operator.call.phone",
|
||||
"consent.personal_data.required",
|
||||
"consent.personal_data.document_url",
|
||||
@@ -182,9 +183,7 @@ def device_snapshot(device: Any) -> dict[str, str | None]:
|
||||
return {
|
||||
"platform": device.platform,
|
||||
"app_version": device.app_version,
|
||||
"device_id_hash": (
|
||||
hashlib.sha256(device.device_id.encode()).hexdigest() if device.device_id else None
|
||||
),
|
||||
"device_id": device.device_id,
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +234,7 @@ async def bootstrap(
|
||||
user_id = (await session.execute(statement)).scalar_one()
|
||||
await session.execute(
|
||||
insert(ClientProfile)
|
||||
.values(id=uuid.uuid4(), user_id=user_id)
|
||||
.values(id=uuid.uuid4(), user_id=user_id, russian_phone=principal.phone_number)
|
||||
.on_conflict_do_nothing(index_elements=[ClientProfile.user_id])
|
||||
)
|
||||
for consent_type in ("personal_data", "user_agreement", "marketing"):
|
||||
@@ -300,7 +299,7 @@ async def record_consents(
|
||||
device = {
|
||||
"platform": ux_session.platform,
|
||||
"app_version": ux_session.app_version,
|
||||
"device_id_hash": ux_session.device_id_hash,
|
||||
"device_id": ux_session.device_id,
|
||||
}
|
||||
now = datetime.now(UTC)
|
||||
versions: dict[str, str] = {}
|
||||
@@ -366,7 +365,7 @@ async def start_session(
|
||||
start_reason=body.start_reason,
|
||||
platform=body.device.platform,
|
||||
app_version=body.device.app_version,
|
||||
device_id_hash=device["device_id_hash"],
|
||||
device_id=device["device_id"],
|
||||
started_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ def test_trace_id_uses_valid_w3c_header_and_generates_fallback() -> None:
|
||||
int(fallback, 16)
|
||||
|
||||
|
||||
def test_user_agent_and_device_are_hashed_without_raw_identifiers() -> None:
|
||||
def test_user_agent_is_hashed_and_device_parameters_are_preserved() -> None:
|
||||
agent = "Example Browser/1.0"
|
||||
assert user_agent_hash(request(user_agent=agent)) == hashlib.sha256(agent.encode()).hexdigest()
|
||||
snapshot = device_snapshot(
|
||||
@@ -77,9 +77,8 @@ def test_user_agent_and_device_are_hashed_without_raw_identifiers() -> None:
|
||||
assert snapshot == {
|
||||
"platform": "web",
|
||||
"app_version": "1.2.3",
|
||||
"device_id_hash": hashlib.sha256(b"raw-device-id").hexdigest(),
|
||||
"device_id": "raw-device-id",
|
||||
}
|
||||
assert "raw-device-id" not in str(snapshot)
|
||||
|
||||
|
||||
def test_audit_copies_request_context_and_bounded_metadata() -> None:
|
||||
|
||||
Reference in New Issue
Block a user