Standalone Shonar Desktop: vendor portable sources + local engine; decouple from ~/Projects/Shonar
- 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
This commit is contained in:
commit
76c867fca4
136 changed files with 21099 additions and 0 deletions
54
backend/shonar/api/v1/users.py
Normal file
54
backend/shonar/api/v1/users.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue