feat: Seaca mobile API, enrollment, FCM push and admin UI (0.9.0)

Adds mobile device registration by admin codes, refresh tokens, push channel
in notification policy, and Settings section for managing Seaca clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-10 13:19:14 +10:00
parent 86c602cf07
commit 563b836acc
32 changed files with 1916 additions and 45 deletions
+44 -33
View File
@@ -67,14 +67,9 @@ bearer_scheme = HTTPBearer(auto_error=False)
class CurrentUser:
username: str
role: str
device_id: int | None = None
@@ -102,22 +97,12 @@ class CurrentUser:
def create_access_token(subject: str, role: str) -> str:
def create_access_token(subject: str, role: str, *, device_id: int | None = None) -> str:
settings = get_settings()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
payload = {"sub": subject, "role": role, "exp": expire}
payload: dict[str, object] = {"sub": subject, "role": role, "exp": expire}
if device_id is not None:
payload["device_id"] = device_id
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
@@ -130,6 +115,33 @@ def create_access_token(subject: str, role: str) -> str:
def _assert_mobile_device_active(db: Session, device_id: int, user_id: int) -> None:
from app.models.mobile_device import MobileDevice
device = db.get(MobileDevice, device_id)
if device is None or device.user_id != user_id or device.revoked_at is not None:
raise HTTPException(status_code=401, detail="Mobile device revoked or invalid")
def get_token_device_id(token: str) -> int | None:
settings = get_settings()
try:
payload = jwt.decode(
token,
settings.jwt_secret,
algorithms=[settings.jwt_algorithm],
)
except JWTError:
return None
raw = payload.get("device_id")
if raw is None:
return None
try:
return int(raw)
except (TypeError, ValueError):
return None
def _user_from_db(db: Session, username: str) -> User:
@@ -206,23 +218,22 @@ def _decode_current_user(token: str, db: Session | None = None) -> CurrentUser:
parsed_device_id: int | None = None
raw_device_id = payload.get("device_id")
if raw_device_id is not None:
try:
parsed_device_id = int(raw_device_id)
except (TypeError, ValueError) as exc:
raise HTTPException(status_code=401, detail="Invalid token") from exc
if db is not None:
user = _user_from_db(db, str(username))
return CurrentUser(username=user.username, role=user.role)
if parsed_device_id is not None:
_assert_mobile_device_active(db, parsed_device_id, user.id)
return CurrentUser(username=user.username, role=user.role, device_id=parsed_device_id)
role = str(payload.get("role") or USER_ROLE_ADMIN)
return CurrentUser(username=str(username), role=role)
return CurrentUser(username=str(username), role=role, device_id=parsed_device_id)