"""DMS Edit-Session/Collabora & Sharing Routen — extrahiert aus routes.py (BUG-018).""" from __future__ import annotations import uuid from fastapi import ( APIRouter, Body, Depends, HTTPException, Response, status, ) from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db from app.core.visibility import check_single_entity_access from app.deps import get_current_user, require_permission from app.plugins.builtins.dms.common import ( OFFICE_EXTENSIONS, _get_file_extension, _parse_uuid, ) from app.plugins.builtins.dms.models import File as DmsFile from app.plugins.builtins.dms.schemas import ShareRemoveRequest, ShareRequest from app.plugins.builtins.permissions.contracts import get_contract as get_perms_contract _perms_contract = get_perms_contract() Permission = _perms_contract.Permission router = APIRouter(tags=["dms"]) @router.post("/files/{file_id}/edit-session", dependencies=[Depends(require_permission("dms:write"))]) async def create_edit_session( file_id: str, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC11: POST /api/v1/dms/files/{id}/edit-session → 200 + Collabora config.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = current_user["user_id"] user_name = current_user.get("name", "Unknown") is_system_admin = current_user.get("role") == "admin" fid = _parse_uuid(file_id, "file_id") result = await db.execute( select(DmsFile).where( DmsFile.id == fid, DmsFile.tenant_id == tenant_id, DmsFile.deleted_at.is_(None), ) ) dms_file = result.scalar_one_or_none() if dms_file is None: raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"}) if not await check_single_entity_access(db, "dms_file", fid, user_id, tenant_id, "write", is_system_admin): raise HTTPException(403, detail={"detail": "Access denied", "code": "forbidden"}) ext = _get_file_extension(dms_file.name) if ext not in OFFICE_EXTENSIONS: raise HTTPException( 400, detail={ "detail": "Only Office files (docx, xlsx, pptx) are supported", "code": "not_office", }, ) file_type = OFFICE_EXTENSIONS[ext] download_url = f"/api/v1/dms/files/{fid}/preview" callback_url = f"/api/v1/dms/files/{fid}/callback" config = { "document": { "fileType": file_type, "key": str(uuid.uuid4()), "title": dms_file.name, "url": download_url, }, "editorConfig": { "mode": "edit", "callbackUrl": callback_url, "user": { "id": user_id, "name": user_name, }, }, } return config # ─── Internal Sharing ─── @router.post("/files/{file_id}/share", dependencies=[Depends(require_permission("dms:share"))]) async def share_file( file_id: str, body: ShareRequest, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC12: POST /api/v1/dms/files/{id}/share → 200, internal share created.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" fid = _parse_uuid(file_id, "file_id") # Verify file exists file_result = await db.execute( select(DmsFile).where( DmsFile.id == fid, DmsFile.tenant_id == tenant_id, DmsFile.deleted_at.is_(None), ) ) if file_result.scalar_one_or_none() is None: raise HTTPException(404, detail={"detail": "File not found", "code": "not_found"}) if not await check_single_entity_access(db, "dms_file", fid, user_id, tenant_id, "share", is_system_admin): raise HTTPException(403, detail={"detail": "Access denied", "code": "forbidden"}) created_perms: list[dict] = [] for uid_str in body.user_ids: uid = _parse_uuid(uid_str, "user_id") # Check if already exists existing = await db.execute( select(Permission).where( Permission.tenant_id == tenant_id, Permission.file_id == fid, Permission.user_id == uid, Permission.access_level == body.access_level, ) ) if existing.scalar_one_or_none() is None: perm = Permission( tenant_id=tenant_id, file_id=fid, user_id=uid, group_id=None, access_level=body.access_level, ) db.add(perm) await db.flush() created_perms.append( { "id": str(perm.id), "file_id": str(fid), "user_id": str(uid), "group_id": None, "access_level": body.access_level, } ) for gid_str in body.group_ids: gid = _parse_uuid(gid_str, "group_id") existing = await db.execute( select(Permission).where( Permission.tenant_id == tenant_id, Permission.file_id == fid, Permission.group_id == gid, Permission.access_level == body.access_level, ) ) if existing.scalar_one_or_none() is None: perm = Permission( tenant_id=tenant_id, file_id=fid, user_id=uuid.UUID(current_user["user_id"]), group_id=gid, access_level=body.access_level, ) db.add(perm) await db.flush() created_perms.append( { "id": str(perm.id), "file_id": str(fid), "user_id": str(perm.user_id), "group_id": str(gid), "access_level": body.access_level, } ) return { "file_id": str(fid), "shared_with": created_perms, "count": len(created_perms), } @router.delete("/files/{file_id}/share", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_permission("dms:share"))]) async def remove_share( file_id: str, body: ShareRemoveRequest = Body(...), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user), ): """AC13: DELETE /api/v1/dms/files/{id}/share → 204, share removed.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) is_system_admin = current_user.get("role") == "admin" fid = _parse_uuid(file_id, "file_id") if not await check_single_entity_access(db, "dms_file", fid, user_id, tenant_id, "share", is_system_admin): raise HTTPException(403, detail={"detail": "Access denied", "code": "forbidden"}) if body.user_id: uid = _parse_uuid(body.user_id, "user_id") result = await db.execute( select(Permission).where( Permission.tenant_id == tenant_id, Permission.file_id == fid, Permission.user_id == uid, ) ) perms = result.scalars().all() for p in perms: await db.delete(p) if body.group_id: gid = _parse_uuid(body.group_id, "group_id") result = await db.execute( select(Permission).where( Permission.tenant_id == tenant_id, Permission.file_id == fid, Permission.group_id == gid, ) ) perms = result.scalars().all() for p in perms: await db.delete(p) await db.flush() return Response(status_code=status.HTTP_204_NO_CONTENT) # ─── Search & Bulk ───