from __future__ import annotations import uuid from fastapi import APIRouter, Depends, File, Form, Header, Request, Response, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from app.core.deps import get_current_user, get_db_session, require_api_permission from app.schemas.collaboration import ( CollaborationCallbackPayload, CollaborationCandidateRead, CollaborationEditorConfigRead, CollaborationEditRequestRead, CollaborationEditRequestResolve, CollaborationExportRecord, CollaborationFileCreate, CollaborationFileRead, CollaborationFileUpdate, CollaborationFolderCreate, CollaborationFolderRead, CollaborationFolderUpdate, CollaborationMemberRead, CollaborationMemberUpsert, CollaborationOwnershipTransferRequest, CollaborationPublicEditorConfigRequest, CollaborationPublicShareMetadata, CollaborationRestoreRequest, CollaborationRevisionCopyRequest, CollaborationRevisionRead, CollaborationRevisionUpdate, CollaborationShareAccessGrant, CollaborationShareLinkRead, CollaborationShareLinkUpdate, CollaborationSharePasswordRequest, ) from app.schemas.onlyoffice import OnlyOfficePreviewConfigRead from app.services import collaboration_service, collaboration_share_service, onlyoffice_collaboration_service, onlyoffice_service router = APIRouter() public_router = APIRouter() internal_router = APIRouter(include_in_schema=False) @router.get( "/folders", response_model=list[CollaborationFolderRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_folders(study_id: uuid.UUID, db: AsyncSession = Depends(get_db_session)): return await collaboration_service.list_folders(db, study_id) @router.post( "/folders", response_model=CollaborationFolderRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("collaboration:manage"))], ) async def create_folder( study_id: uuid.UUID, payload: CollaborationFolderCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): return await collaboration_service.create_folder(db, study_id, payload, current_user) @router.patch( "/folders/{folder_id}", response_model=CollaborationFolderRead, dependencies=[Depends(require_api_permission("collaboration:manage"))], ) async def update_folder( study_id: uuid.UUID, folder_id: uuid.UUID, payload: CollaborationFolderUpdate, db: AsyncSession = Depends(get_db_session), ): return await collaboration_service.update_folder(db, study_id, folder_id, payload) @router.delete( "/folders/{folder_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:manage"))], ) async def delete_folder(study_id: uuid.UUID, folder_id: uuid.UUID, db: AsyncSession = Depends(get_db_session)): await collaboration_service.delete_folder(db, study_id, folder_id) @router.get( "/files", response_model=list[CollaborationFileRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_files( study_id: uuid.UUID, folder_id: uuid.UUID | None = None, keyword: str | None = None, deleted: bool = False, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): return await collaboration_service.list_files( db, study_id, current_user, folder_id=folder_id, keyword=keyword, deleted=deleted ) @router.post( "/files", response_model=CollaborationFileRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("collaboration:create"))], ) async def create_file( study_id: uuid.UUID, payload: CollaborationFileCreate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.create_blank_file(db, study_id, payload, current_user) return await collaboration_service.file_read(db, item, current_user) @router.post( "/files/import", response_model=CollaborationFileRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("collaboration:create"))], ) async def import_file( study_id: uuid.UUID, file: UploadFile = File(...), folder_id: uuid.UUID | None = Form(None), db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.import_file(db, study_id, folder_id, file, current_user) return await collaboration_service.file_read(db, item, current_user) @router.post( "/files/{file_id}/copy", response_model=CollaborationFileRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("collaboration:create"))], ) async def copy_file( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): source = await collaboration_service.get_file_or_404(db, study_id, file_id) item = await collaboration_service.copy_file(db, source, current_user) return await collaboration_service.file_read(db, item, current_user) @router.get( "/files/{file_id}", response_model=CollaborationFileRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def get_file( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.file_read(db, item, current_user) @router.get( "/files/{file_id}/download", dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def download_file( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) revision = await collaboration_service.prepare_download(db, item, current_user) return FileResponse( path=revision.file_uri, media_type=revision.mime_type, filename=item.title, content_disposition_type="attachment", headers={"Cache-Control": "no-store"}, ) @router.patch( "/files/{file_id}", response_model=CollaborationFileRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def update_file( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationFileUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) item = await collaboration_service.update_file(db, item, payload, current_user) return await collaboration_service.file_read(db, item, current_user) @router.delete( "/files/{file_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def trash_file( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) await collaboration_service.move_to_trash(db, item, current_user) @router.post( "/files/{file_id}/restore", response_model=CollaborationFileRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def restore_file( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id, include_deleted=True) item = await collaboration_service.restore_file(db, item, current_user) return await collaboration_service.file_read(db, item, current_user) @router.get( "/files/{file_id}/members", response_model=list[CollaborationMemberRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_members(study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session)): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.list_members(db, item) @router.put( "/files/{file_id}/members", response_model=CollaborationMemberRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def upsert_member( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationMemberUpsert, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.upsert_member(db, item, payload, current_user) @router.delete( "/files/{file_id}/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def remove_member( study_id: uuid.UUID, file_id: uuid.UUID, user_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) await collaboration_service.remove_member(db, item, user_id, current_user) @router.post( "/files/{file_id}/edit-requests", response_model=CollaborationEditRequestRead, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def create_edit_request( study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.create_edit_request(db, item, current_user) @router.get( "/files/{file_id}/edit-requests", response_model=list[CollaborationEditRequestRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_edit_requests( study_id: uuid.UUID, file_id: uuid.UUID, pending_only: bool = True, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.list_edit_requests(db, item, current_user, pending_only=pending_only) @router.post( "/files/{file_id}/edit-requests/{request_id}/resolve", response_model=CollaborationEditRequestRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def resolve_edit_request( study_id: uuid.UUID, file_id: uuid.UUID, request_id: uuid.UUID, payload: CollaborationEditRequestResolve, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.resolve_edit_request(db, item, request_id, payload, current_user) @router.post( "/files/{file_id}/transfer-ownership", response_model=CollaborationFileRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def transfer_ownership( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationOwnershipTransferRequest, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) item = await collaboration_service.transfer_ownership(db, item, payload, current_user) return await collaboration_service.file_read(db, item, current_user) @router.get( "/files/{file_id}/share-link", response_model=CollaborationShareLinkRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def get_share_link( study_id: uuid.UUID, file_id: uuid.UUID, response: Response, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) result = await collaboration_share_service.get_share_link(db, item, current_user) response.headers["Cache-Control"] = "no-store" return result @router.put( "/files/{file_id}/share-link", response_model=CollaborationShareLinkRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def update_share_link( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationShareLinkUpdate, response: Response, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) result = await collaboration_share_service.update_share_link(db, item, payload, current_user) response.headers["Cache-Control"] = "no-store" return result @router.get( "/member-candidates", response_model=list[CollaborationCandidateRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_member_candidates(study_id: uuid.UUID, db: AsyncSession = Depends(get_db_session)): return await collaboration_service.list_candidates(db, study_id) @router.get( "/files/{file_id}/revisions", response_model=list[CollaborationRevisionRead], dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def list_revisions(study_id: uuid.UUID, file_id: uuid.UUID, db: AsyncSession = Depends(get_db_session)): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.list_revisions(db, item) @router.patch( "/files/{file_id}/revisions/{revision_id}", response_model=CollaborationRevisionRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def update_revision( study_id: uuid.UUID, file_id: uuid.UUID, revision_id: uuid.UUID, payload: CollaborationRevisionUpdate, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.update_revision(db, item, revision_id, payload, current_user) @router.delete( "/files/{file_id}/revisions/{revision_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def delete_revision( study_id: uuid.UUID, file_id: uuid.UUID, revision_id: uuid.UUID, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) await collaboration_service.delete_revision(db, item, revision_id, current_user) @router.post( "/files/{file_id}/revisions/{revision_id}/copy", response_model=CollaborationFileRead, status_code=status.HTTP_201_CREATED, dependencies=[ Depends(require_api_permission("collaboration:create")), Depends(require_api_permission("collaboration:read")), ], ) async def copy_revision( study_id: uuid.UUID, file_id: uuid.UUID, revision_id: uuid.UUID, payload: CollaborationRevisionCopyRequest, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) copied = await collaboration_service.copy_revision(db, item, revision_id, payload, current_user) return await collaboration_service.file_read(db, copied, current_user) @router.get( "/files/{file_id}/revisions/{revision_id}/preview-config", response_model=OnlyOfficePreviewConfigRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def get_revision_preview_config( study_id: uuid.UUID, file_id: uuid.UUID, revision_id: uuid.UUID, response: Response, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) revision = await collaboration_service.prepare_revision_preview(db, item, revision_id, current_user) await onlyoffice_service.ensure_onlyoffice_available() result = onlyoffice_service.build_preview_config( resource_type="collaboration_revision", resource_id=revision.id, file_name=item.title, file_hash=revision.file_hash, user_id=current_user.id, user_name=current_user.full_name, ) response.headers["Cache-Control"] = "no-store" return result @router.post( "/files/{file_id}/revisions/{revision_id}/restore", response_model=CollaborationRevisionRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def restore_revision( study_id: uuid.UUID, file_id: uuid.UUID, revision_id: uuid.UUID, payload: CollaborationRestoreRequest, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) return await collaboration_service.restore_revision( db, item, revision_id, current_user, payload.change_summary ) @router.get( "/files/{file_id}/editor-config", response_model=CollaborationEditorConfigRead, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def get_editor_config( study_id: uuid.UUID, file_id: uuid.UUID, response: Response, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) result = await onlyoffice_collaboration_service.build_editor_config(db, item, current_user) response.headers["Cache-Control"] = "no-store" return result @router.post( "/files/{file_id}/exports", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def record_export( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationExportRecord, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) await collaboration_service.record_export(db, item, current_user, payload.file_type) @router.post( "/files/{file_id}/downloads", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_api_permission("collaboration:read"))], ) async def record_download( study_id: uuid.UUID, file_id: uuid.UUID, payload: CollaborationExportRecord, db: AsyncSession = Depends(get_db_session), current_user=Depends(get_current_user), ): item = await collaboration_service.get_file_or_404(db, study_id, file_id) await collaboration_service.record_download(db, item, current_user, payload.file_type) return Response(status_code=status.HTTP_204_NO_CONTENT) @public_router.get("/metadata", response_model=CollaborationPublicShareMetadata) async def get_public_share_metadata( response: Response, x_ctms_share_token: str | None = Header(default=None, alias="X-CTMS-Share-Token"), db: AsyncSession = Depends(get_db_session), ): result = await collaboration_share_service.public_metadata(db, x_ctms_share_token) response.headers["Cache-Control"] = "no-store" return result @public_router.post("/access", response_model=CollaborationShareAccessGrant) async def verify_public_share_password( payload: CollaborationSharePasswordRequest, response: Response, x_ctms_share_token: str | None = Header(default=None, alias="X-CTMS-Share-Token"), db: AsyncSession = Depends(get_db_session), ): result = await collaboration_share_service.verify_share_password( db, x_ctms_share_token, payload.password ) response.headers["Cache-Control"] = "no-store" return result @public_router.post("/editor-config", response_model=CollaborationEditorConfigRead) async def get_public_share_editor_config( payload: CollaborationPublicEditorConfigRequest, response: Response, x_ctms_share_token: str | None = Header(default=None, alias="X-CTMS-Share-Token"), db: AsyncSession = Depends(get_db_session), ): link, item = await collaboration_share_service.resolve_active_share(db, x_ctms_share_token) collaboration_share_service.validate_access_grant(link, payload.access_token) result = await onlyoffice_collaboration_service.build_shared_editor_config( db, item, link, client_id=payload.client_id, display_name=payload.display_name, ) response.headers["Cache-Control"] = "no-store" return result @internal_router.get("/internal/onlyoffice/collaboration/sessions/{session_id}/content") async def get_session_content(session_id: uuid.UUID, request: Request, db: AsyncSession = Depends(get_db_session)): revision, item = await onlyoffice_collaboration_service.get_session_content( db, session_id, request.headers.get("AuthorizationJwt") ) return FileResponse( path=revision.file_uri, media_type=revision.mime_type, filename=item.title, content_disposition_type="inline", ) @internal_router.post("/internal/onlyoffice/collaboration/sessions/{session_id}/callback") async def collaboration_callback( session_id: uuid.UUID, payload: CollaborationCallbackPayload, request: Request, db: AsyncSession = Depends(get_db_session), ): onlyoffice_collaboration_service.validate_callback_token( request.headers.get("AuthorizationJwt"), payload ) return await onlyoffice_collaboration_service.process_callback(db, session_id, payload)