"""Self-service surface: the systems I am accountable for. Mounts under ``/api/v1/me/systems``. No role gate — the scope IS the authorisation: you see the rows where you are the owner or the backup owner, and nothing else. This is the read that makes the register honest from the employee's side: if someone is listed as owning something they have never heard of, they need to be able to see that and say so. """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, Request from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.auth.models import AuthUser from app.core.database import get_db from app.core.dependencies import get_current_user from app.core.exceptions import ForbiddenError from app.core.response import success_response from app.domains.system_registry import constants as const from app.domains.system_registry import notify from app.domains.system_registry import service as svc from app.domains.system_registry.serializers import serialize_system router = APIRouter(prefix="/api/v1/me/systems", tags=["system-registry"]) @router.get("") async def my_systems( request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): rows = await svc.systems_owned_by(db, user.id) payload = [] for system in rows: item = serialize_system(system, include_notes=False) # Which hat the caller is wearing on this row. The offboarding # handover only forces a transfer of PRIMARY ownership, so the # distinction is not cosmetic. item["my_role"] = ( "owner" if system.owner_person_id == user.id else "backup_owner" ) payload.append(item) return success_response( { "items": payload, "owned": len( [p for p in payload if p["my_role"] == "owner"], ), "backup": len( [p for p in payload if p["my_role"] == "backup_owner"], ), }, request=request, ) class DisputeRequest(BaseModel): note: str | None = Field(None, max_length=const.MAX_NOTES_CHARS) @router.post("/{system_id}/dispute") async def dispute_ownership( system_id: uuid.UUID, payload: DisputeRequest, request: Request, user: AuthUser = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """「这不该由我负责」. The employee cannot re-point the row — ownership is an accountability record maintained by IT, not a preference. What they can do is contest it, which writes the claim onto the system's own trail (so it is visible next to the ownership history it disputes) and pushes it to the people who can actually fix it. Self-scope: you may only contest a row that names YOU. 403 otherwise — being able to file "not mine" against a colleague's system would make the trail unreliable in exactly the place it has to be trusted. """ system = await svc.get_system(db, system_id) if user.id == system.owner_person_id: role = "owner" elif user.id == system.backup_owner_person_id: role = "backup_owner" else: raise ForbiddenError( "You can only dispute ownership of a system that names you.", ) actor = await svc.load_person_optional(db, user.id) await svc.add_event( db, system, event_type=const.EVENT_OWNER_DISPUTED, actor=actor, body=(payload.note or "").strip() or None, meta={"role": role}, ) await db.commit() # Best-effort: a Feishu hiccup must not lose the trail entry that was # just committed, so the push happens after the commit and its result # is reported rather than assumed. reached = await notify.notify_ownership_disputed( db, system, disputer=actor or user, role=role, note=payload.note, ) return success_response( {"recorded": True, "notified_operators": reached}, request=request, )