Se aniado el boton para subir fotografias desde el equipo, ademas de guardardo en minio
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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/...).
|
||||
|
||||
Reference in New Issue
Block a user