import json import os import uuid from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import FileResponse from sqlalchemy.orm import Session, joinedload from app.auth import get_current_user from app.config import settings from app.database import get_db from app.models.document import FilledDocument from app.models.template import DocumentTemplate from app.models.user import User from app.schemas import ( FilledDocumentCreate, FilledDocumentResponse, FilledDocumentUpdate, PreviewResponse, ) from app.services.access import accessible_document, accessible_template, is_admin from app.services.audit import get_client_ip, log_action from app.services.docx_renderer import docx_to_html, export_to_pdf, render_docx router = APIRouter(prefix="/api/documents", tags=["documents"]) def _doc_to_response(doc: FilledDocument) -> FilledDocumentResponse: field_data = json.loads(doc.field_data) return FilledDocumentResponse( id=doc.id, template_id=doc.template_id, owner_id=doc.owner_id, owner_username=doc.owner.username if doc.owner else None, name=doc.name, field_data=field_data, created_at=doc.created_at, ) def _get_template_for_document( db: Session, user: User, template_id: int ) -> DocumentTemplate: template = accessible_template(db, user, template_id) if not template: raise HTTPException(status_code=404, detail="Template not found") return template def _rerender_document(doc: FilledDocument, template: DocumentTemplate) -> None: field_data = json.loads(doc.field_data) output_path = doc.rendered_docx_path or os.path.join( settings.export_dir, f"{uuid.uuid4().hex}_rendered.docx" ) os.makedirs(settings.export_dir, exist_ok=True) render_docx(template.file_path, field_data, output_path) doc.rendered_docx_path = output_path @router.get("", response_model=list[FilledDocumentResponse]) def list_documents( db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): query = db.query(FilledDocument).options(joinedload(FilledDocument.owner)) if not is_admin(current_user): query = query.filter(FilledDocument.owner_id == current_user.id) docs = query.order_by(FilledDocument.created_at.desc()).all() return [_doc_to_response(d) for d in docs] @router.get("/{document_id}", response_model=FilledDocumentResponse) def get_document( document_id: int, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = ( db.query(FilledDocument) .options(joinedload(FilledDocument.owner)) .filter(FilledDocument.id == document_id) .first() ) if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") return _doc_to_response(doc) @router.patch("/{document_id}", response_model=FilledDocumentResponse) def update_document( document_id: int, update: FilledDocumentUpdate, request: Request, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = ( db.query(FilledDocument) .options(joinedload(FilledDocument.owner)) .filter(FilledDocument.id == document_id) .first() ) if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if update.name is not None: doc.name = update.name if update.field_data is not None: doc.field_data = json.dumps(update.field_data) template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template not found") _rerender_document(doc, template) log_action( db, "document.update", user=current_user, resource_type="document", resource_id=doc.id, details={"name": doc.name}, ip_address=get_client_ip(request), ) db.commit() db.refresh(doc) return _doc_to_response(doc) @router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_document( document_id: int, request: Request, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") log_action( db, "document.delete", user=current_user, resource_type="document", resource_id=doc.id, details={"name": doc.name}, ip_address=get_client_ip(request), ) if doc.rendered_docx_path and os.path.exists(doc.rendered_docx_path): os.remove(doc.rendered_docx_path) db.delete(doc) db.commit() @router.post("", response_model=FilledDocumentResponse, status_code=status.HTTP_201_CREATED) def create_document( data: FilledDocumentCreate, request: Request, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): template = _get_template_for_document(db, current_user, data.template_id) os.makedirs(settings.export_dir, exist_ok=True) output_name = f"{uuid.uuid4().hex}_rendered.docx" output_path = os.path.join(settings.export_dir, output_name) render_docx(template.file_path, data.field_data, output_path) doc = FilledDocument( template_id=template.id, owner_id=current_user.id, name=data.name, field_data=json.dumps(data.field_data), rendered_docx_path=output_path, ) db.add(doc) db.flush() log_action( db, "document.create", user=current_user, resource_type="document", resource_id=doc.id, details={"name": doc.name, "template_id": doc.template_id}, ip_address=get_client_ip(request), ) db.commit() db.refresh(doc) return _doc_to_response(doc) @router.post("/preview", response_model=PreviewResponse) def preview_document( data: FilledDocumentCreate, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): template = _get_template_for_document(db, current_user, data.template_id) os.makedirs(settings.export_dir, exist_ok=True) temp_path = os.path.join(settings.export_dir, f"preview_{uuid.uuid4().hex}.docx") try: render_docx(template.file_path, data.field_data, temp_path) html = docx_to_html(temp_path) return PreviewResponse(html=html, field_data=data.field_data) finally: if os.path.exists(temp_path): os.remove(temp_path) @router.get("/{document_id}/export/docx") def export_docx( document_id: int, request: Request, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if not doc.rendered_docx_path or not os.path.exists(doc.rendered_docx_path): template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if template: _rerender_document(doc, template) db.commit() if not doc.rendered_docx_path: raise HTTPException(status_code=404, detail="Rendered file not found") log_action( db, "document.export_docx", user=current_user, resource_type="document", resource_id=doc.id, details={"name": doc.name}, ip_address=get_client_ip(request), ) db.commit() return FileResponse( doc.rendered_docx_path, media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", filename=f"{doc.name}.docx", ) @router.get("/{document_id}/export/pdf") def export_pdf( document_id: int, request: Request, db: Annotated[Session, Depends(get_db)], current_user: Annotated[User, Depends(get_current_user)], ): doc = db.query(FilledDocument).filter(FilledDocument.id == document_id).first() if not doc or not accessible_document(db, current_user, document_id): raise HTTPException(status_code=404, detail="Document not found") if not doc.rendered_docx_path or not os.path.exists(doc.rendered_docx_path): template = db.query(DocumentTemplate).filter( DocumentTemplate.id == doc.template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template not found") _rerender_document(doc, template) db.commit() docx_path = doc.rendered_docx_path if not docx_path: raise HTTPException(status_code=404, detail="Rendered file not found") pdf_path = docx_path.replace(".docx", ".pdf") try: export_to_pdf(docx_path, pdf_path) except RuntimeError as e: raise HTTPException(status_code=500, detail=str(e)) log_action( db, "document.export_pdf", user=current_user, resource_type="document", resource_id=doc.id, details={"name": doc.name}, ip_address=get_client_ip(request), ) db.commit() return FileResponse(pdf_path, media_type="application/pdf", filename=f"{doc.name}.pdf")