Se aniado el boton para subir fotografias desde el equipo, ademas de guardardo en minio

This commit is contained in:
2026-06-01 17:13:00 -05:00
parent 2ccf1b4879
commit af6c0698da
4 changed files with 382 additions and 28 deletions

View File

@@ -2,17 +2,27 @@
Rutas para gestión de firmas
"""
import mimetypes
import os
from datetime import datetime
from typing import Any, Dict
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
from fastapi import Depends, HTTPException, Query, status
from fastapi import Depends, File, HTTPException, Query, Response, UploadFile, status
from sqlalchemy.orm import Session
from core.config import settings
from core.database import get_core_db
from core.s3_keys import signature_photo_key
from core.storage_s3 import delete_object_if_exists, get_object_bytes, put_object_bytes
from .dto import SignatureCreateDTO, SignatureResponseDTO, SignatureUpdateDTO
from .service import SignatureService
# Constantes
ALLOWED_PHOTO_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}
MAX_PHOTO_SIZE = 5 * 1024 * 1024 # 5 MB
# Create router using TenantCRUDRoutes factory
signature_crud = TenantCRUDRoutes(
service=SignatureService,
@@ -81,3 +91,119 @@ async def delete_signature(
detail="Signature not found",
)
return None
@router.post(
"/{signature_id}/upload-photo",
response_model=dict,
summary="Upload signature photo",
)
async def upload_signature_photo(
signature_id: int,
company_id: int = Query(..., description="Company ID"),
file: UploadFile = File(...),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(signature_crud.auth_dependency),
):
"""
Sube la foto de la firma electrónica a MinIO/S3 y actualiza photo_path en la firma.
Extensiones permitidas: .jpg, .jpeg, .png, .gif, .webp — máx 5 MB.
"""
tenant_id = validate_access_to_resource(
db, company_id, current_user, ["cat_signatures.edit"]
)
# Verificar que la firma existe
sig = SignatureService.get_by_id(db, signature_id, tenant_id, company_id)
if not sig:
raise HTTPException(status_code=404, detail="Signature not found")
# Validar extensión
file_ext = os.path.splitext(file.filename or "")[1].lower()
if file_ext not in ALLOWED_PHOTO_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"Tipo de archivo no permitido. Permitidos: {', '.join(ALLOWED_PHOTO_EXTENSIONS)}",
)
# Leer y validar tamaño
content = await file.read()
if len(content) > MAX_PHOTO_SIZE:
raise HTTPException(
status_code=400,
detail=f"Archivo demasiado grande. Máximo: {MAX_PHOTO_SIZE // 1024 // 1024} MB",
)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
try:
if settings.use_s3_object_storage:
# Eliminar foto anterior si existe en S3
if sig.photo_path and sig.photo_path.startswith("tenants/"):
delete_object_if_exists(sig.photo_path)
key = signature_photo_key(tenant_id, company_id, signature_id, timestamp, file_ext)
ct = mimetypes.guess_type(file.filename or "photo")[0] or "image/jpeg"
put_object_bytes(key, content, content_type=ct)
file_path = key
else:
# Fallback: almacenamiento local
upload_dir = os.path.join("uploads", "signatures", str(signature_id))
os.makedirs(upload_dir, exist_ok=True)
filename = f"photo_{timestamp}{file_ext}"
file_path = os.path.join(upload_dir, filename)
with open(file_path, "wb") as f:
f.write(content)
except Exception as exc:
raise HTTPException(
status_code=500,
detail=f"Error al guardar la foto: {str(exc)}",
) from exc
# Actualizar photo_path en la firma
from .dto import SignatureUpdateDTO as UpdateDTO
SignatureService.update(
db, signature_id, tenant_id, UpdateDTO(photo_path=file_path), company_id
)
return {
"message": "Foto subida correctamente",
"photo_path": file_path,
"signature_id": signature_id,
}
@router.get(
"/{signature_id}/photo",
summary="Get signature photo image",
)
async def get_signature_photo(
signature_id: int,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(signature_crud.auth_dependency),
):
"""Devuelve los bytes de la foto de la firma desde MinIO/S3 para preview."""
tenant_id = validate_access_to_resource(
db, company_id, current_user, ["cat_signatures.view"]
)
sig = SignatureService.get_by_id(db, signature_id, tenant_id, company_id)
if not sig or not sig.photo_path:
raise HTTPException(status_code=404, detail="Foto no encontrada")
if sig.photo_path.startswith("tenants/"):
try:
data = get_object_bytes(sig.photo_path)
except Exception:
raise HTTPException(status_code=404, detail="Foto no encontrada en el almacenamiento")
media_type = mimetypes.guess_type(sig.photo_path)[0] or "image/jpeg"
return Response(content=data, media_type=media_type)
# Fallback: archivo local
if not os.path.isfile(sig.photo_path):
raise HTTPException(status_code=404, detail="Foto no encontrada en el sistema de archivos")
with open(sig.photo_path, "rb") as f:
data = f.read()
media_type = mimetypes.guess_type(sig.photo_path)[0] or "image/jpeg"
return Response(content=data, media_type=media_type)

View File

@@ -396,6 +396,26 @@ def help_public_api_path(relative_under_help: str) -> str:
return f"/api/v1/core/help-center/files/{rel}"
def signature_photo_key(
tenant_id: Union[int, str],
company_id: int,
signature_id: int,
timestamp: str,
file_ext: str,
) -> str:
"""
Foto de firma bajo ``.../signatures/{signature_id}/photo_{timestamp}.{ext}``.
Extensiones permitidas: .jpg, .jpeg, .png, .gif, .webp
"""
sid = _segment(signature_id, "signature_id")
ts = _segment(timestamp, "timestamp")
ext = file_ext.lower() if str(file_ext).startswith(".") else f".{file_ext}"
allowed = (".jpg", ".jpeg", ".png", ".gif", ".webp")
if ext not in allowed:
raise ValueError(f"signature photo extension not allowed: {ext}")
return f"{tenant_company_prefix(tenant_id, company_id)}signatures/{sid}/photo_{ts}{ext}"
def system_help_object_key(relative_path: str) -> str:
"""
Clave S3 completa bajo ``system/help/`` para un path relativo (p. ej. GET /files/...).