fix(mobile): повторный enroll и удаление устройств в UI (0.9.10).

При повторной привязке Seaca переиспользуется запись device_uuid вместо INSERT.
Добавлены POST /devices/{id}/revoke и DELETE для полного удаления записи в настройках SAC.
This commit is contained in:
2026-06-13 16:36:07 +10:00
parent e022f2f213
commit 3d36faa49d
10 changed files with 186 additions and 39 deletions
+16 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
@@ -13,6 +13,7 @@ from app.services.mobile_devices import (
get_device_or_raise,
list_mobile_devices,
revoke_device,
delete_device_record,
touch_device,
update_fcm_token,
assert_device_active,
@@ -257,8 +258,8 @@ def get_devices(
return [_device_to_response(item) for item in list_mobile_devices(db)]
@router.delete("/devices/{device_id}", response_model=MobileDeviceResponse)
def delete_device(
@router.post("/devices/{device_id}/revoke", response_model=MobileDeviceResponse)
def post_revoke_device(
device_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
@@ -270,6 +271,18 @@ def delete_device(
return _device_to_response(summary)
@router.delete("/devices/{device_id}", status_code=204)
def delete_device(
device_id: int,
db: Session = Depends(get_db),
_user=Depends(require_admin),
) -> None:
try:
delete_device_record(db, device_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.post("/devices/{device_id}/test-push", response_model=TestPushResponse)
def test_device_push(
device_id: int,
+8 -1
View File
@@ -1,4 +1,4 @@
"""Управление зарегистрированными устройствами Seaca."""
"""Управление зарегистрированными устройствами Seaca."""
from __future__ import annotations
@@ -86,6 +86,13 @@ def revoke_device(db: Session, device_id: int) -> MobileDeviceSummary:
return _to_summary(device, username)
def delete_device_record(db: Session, device_id: int) -> None:
device = get_device_or_raise(db, device_id)
revoke_refresh_tokens_for_device(db, device.id)
db.delete(device)
db.commit()
def update_fcm_token(db: Session, device: MobileDevice, fcm_token: str) -> None:
cleaned = (fcm_token or "").strip()
if not cleaned:
+40 -21
View File
@@ -1,4 +1,4 @@
"""Создание и проверка кодов регистрации Seaca."""
"""Создание и проверка кодов регистрации Seaca."""
from __future__ import annotations
@@ -223,31 +223,50 @@ def enroll_device(
if code_row.target_user_id is not None and user.id != code_row.target_user_id:
raise ValueError("enrollment code is bound to another user")
existing = db.scalar(select(MobileDevice).where(MobileDevice.device_uuid == cleaned_uuid))
if existing is not None and existing.user_id != user.id:
raise ValueError("device_uuid already registered to another user")
active_count = _count_active_devices(db, user.id)
if (
existing is not None
and existing.user_id == user.id
and existing.revoked_at is None
):
active_count -= 1
if active_count >= mobile_cfg.max_devices_per_user:
raise ValueError("device limit reached for this user")
existing = db.scalar(select(MobileDevice).where(MobileDevice.device_uuid == cleaned_uuid))
if existing is not None:
if existing.revoked_at is None and existing.user_id != user.id:
raise ValueError("device_uuid already registered to another user")
if existing.revoked_at is None:
revoke_refresh_tokens_for_device(db, existing.id)
existing.revoked_at = now
existing = None
cleaned_display = (display_name or "").strip() or "Android"
cleaned_platform = (platform or "android").strip() or "android"
cleaned_app_version = (app_version or "").strip() or None
cleaned_fcm = (fcm_token or "").strip() or None
device = MobileDevice(
user_id=user.id,
device_uuid=cleaned_uuid,
display_name=(display_name or "").strip() or "Android",
platform=(platform or "android").strip() or "android",
app_version=(app_version or "").strip() or None,
fcm_token=(fcm_token or "").strip() or None,
fcm_token_updated_at=now if fcm_token else None,
enrollment_code_id=code_row.id,
last_seen_at=now,
)
db.add(device)
if existing is not None:
revoke_refresh_tokens_for_device(db, existing.id)
existing.revoked_at = None
existing.user_id = user.id
existing.display_name = cleaned_display
existing.platform = cleaned_platform
existing.app_version = cleaned_app_version
existing.fcm_token = cleaned_fcm
existing.fcm_token_updated_at = now if cleaned_fcm else None
existing.enrollment_code_id = code_row.id
existing.last_seen_at = now
device = existing
else:
device = MobileDevice(
user_id=user.id,
device_uuid=cleaned_uuid,
display_name=cleaned_display,
platform=cleaned_platform,
app_version=cleaned_app_version,
fcm_token=cleaned_fcm,
fcm_token_updated_at=now if cleaned_fcm else None,
enrollment_code_id=code_row.id,
last_seen_at=now,
)
db.add(device)
code_row.use_count += 1
db.flush()
+2 -2
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center"
APP_VERSION = "0.9.9"
APP_VERSION = "0.9.10"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"