- shared/ = portable Android-origin sources vendored from deferred/desktop-server (app/build.gradle.kts srcDir repointed; PlaybackController.kt excluded as Android-only) - backend/ = bundled-lite engine (SQLite + inline queue); .venv symlinked from the old checkout, PYTHONPATH pins THIS backend's code over any editable install - repoRoot() resolves this project dir (env SHONAR_REPO still wins); desktop-dev.sh watches shared/ + backend/ - Verified: :app:compileKotlin + :app:test green (23 tests); engine boots on :8010, self-migrates, /healthz ok
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""Users and devices."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
from sqlalchemy import select, update
|
|
|
|
from shonar.api.deps import CurrentUser, SessionDep
|
|
from shonar.api.schemas_common import DeviceOut, UserOut, UserUpdate
|
|
from shonar.db.models import Device, RefreshToken, utcnow
|
|
|
|
router = APIRouter(tags=["users", "devices"])
|
|
|
|
|
|
@router.get("/users/me", response_model=UserOut)
|
|
async def get_me(user: CurrentUser):
|
|
return user
|
|
|
|
|
|
@router.patch("/users/me", response_model=UserOut)
|
|
async def update_me(body: UserUpdate, user: CurrentUser, session: SessionDep):
|
|
if body.display_name is not None:
|
|
user.display_name = body.display_name
|
|
if body.location_storage_enabled is not None:
|
|
user.location_storage_enabled = body.location_storage_enabled
|
|
await session.flush()
|
|
return user
|
|
|
|
|
|
@router.get("/devices", response_model=list[DeviceOut])
|
|
async def list_devices(user: CurrentUser, session: SessionDep):
|
|
rows = await session.scalars(
|
|
select(Device).where(Device.user_id == user.id).order_by(Device.last_seen_at.desc())
|
|
)
|
|
return list(rows)
|
|
|
|
|
|
@router.delete("/devices/{device_id}", status_code=204)
|
|
async def revoke_device(device_id: uuid.UUID, user: CurrentUser, session: SessionDep):
|
|
device = await session.get(Device, device_id)
|
|
# Ownership check — no cross-user access, and 404 (not 403) to avoid
|
|
# leaking existence.
|
|
if device is None or device.user_id != user.id:
|
|
raise HTTPException(404, "Device not found")
|
|
device.revoked_at = utcnow()
|
|
# Revoke this device's live refresh tokens.
|
|
await session.execute(
|
|
update(RefreshToken)
|
|
.where(RefreshToken.device_id == device.id, RefreshToken.revoked_at.is_(None))
|
|
.values(revoked_at=utcnow())
|
|
)
|
|
return None
|