ya firma bien

This commit is contained in:
David Arranz 2026-02-03 16:23:25 +01:00
parent 1a76b4aa22
commit 3d93887e4e
10 changed files with 108 additions and 47 deletions

View File

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "factuges-document-signing-service"
version = "0.1.3"
version = "0.2.4"
description = "FastAPI service for signing PDF documents using external secret managers"
requires-python = ">=3.11"

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_VERSION="0.0.3"
SCRIPT_VERSION="0.4"
# =====================================================
# FACTUGES Document Signing Service Build Script
@ -64,9 +64,27 @@ if [[ $COMPANY =~ --.* ]]; then
exit 1
fi
# --- Detectar nombre y versión de la API ---
IMAGE_NAME=$(node -p "require('${PROJECT_DIR}/setup.cfg').name" 2>/dev/null || echo "factuges-document-signing-service")
IMAGE_VERSION=$(node -p "require('${PROJECT_DIR}/setup.cfg').version" 2>/dev/null || echo "0.1.3")
# --- Detectar nombre y versión ---
IMAGE_NAME=$(python - <<'EOF'
import tomllib
with open("../pyproject.toml", "rb") as f:
data = tomllib.load(f)
print(data["project"]["name"])
EOF
)
IMAGE_VERSION=$(python - <<'EOF'
import tomllib
with open("../pyproject.toml", "rb") as f:
data = tomllib.load(f)
print(data["project"]["version"])
EOF
)
PORT="8000" # valor por defecto

View File

@ -1,15 +0,0 @@
[metadata]
name = "factuges-document-signing-service"
version = "0.1.3"
description = "FastAPI service for signing PDF documents using external secret managers"
author = Rodax Software
author_email = info@rodax-software.com
long_description = file: README.md
long_description_content_type = text/markdown
url = https://factuges.app
license = "Propietaria"
classifiers =
Programming Language :: Python :: 3.11
License :: OSI Approved :: MIT License
Operating System :: OS Independent

View File

@ -3,6 +3,6 @@ from importlib.metadata import version, PackageNotFoundError
def get_package_version() -> str:
try:
return version("factuges-document-signing-service") # nombre del paquete en [metadata].name
return version("factuges-document-signing-service")
except PackageNotFoundError:
return "unknown"

View File

@ -0,0 +1,43 @@
import io
from pyhanko.pdf_utils.reader import PdfFileReader
from pyhanko.pdf_utils.misc import PdfReadError
# from pyhanko.sign.general import find_signatures
from signing_service.infrastructure.pdf.pdf_validator_error import PDFValidationError
class PDFValidator:
MIN_PDF_SIZE_BYTES = 100
def validate(self, pdf_bytes: bytes) -> None:
self._validate_not_empty(pdf_bytes)
reader = self._load_pdf(pdf_bytes)
self._validate_not_encrypted(reader)
self._validate_has_pages(reader)
# self._validate_not_signed(reader)
def _validate_not_empty(self, pdf_bytes: bytes) -> None:
if not pdf_bytes or len(pdf_bytes) < self.MIN_PDF_SIZE_BYTES:
raise PDFValidationError("PDF vacío o demasiado pequeño")
def _load_pdf(self, pdf_bytes: bytes) -> PdfFileReader:
try:
return PdfFileReader(io.BytesIO(pdf_bytes))
except PdfReadError as exc:
raise PDFValidationError("El archivo no es un PDF válido") from exc
def _validate_not_encrypted(self, reader: PdfFileReader) -> None:
if reader.encrypted:
raise PDFValidationError("PDF cifrado no soportado")
def _validate_has_pages(self, reader: PdfFileReader) -> None:
if reader.root['/Pages']['/Count'] == 0:
raise PDFValidationError("El PDF no contiene páginas")
""" def _validate_not_signed(self, reader: PdfFileReader) -> None:
signatures = list(find_signatures(reader))
if signatures:
raise PDFValidationError("El PDF ya contiene firmas") """

View File

@ -0,0 +1,2 @@
class PDFValidationError(Exception):
pass

View File

@ -1,16 +1,16 @@
import base64
import io
from datetime import datetime, timezone
from pyhanko import stamp
from pyhanko.sign import fields, signers
from pyhanko.sign import signers
from pyhanko.pdf_utils import text
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from signing_service.domain.ports.pdf_signer import PDFSignerPort
from signing_service.infrastructure.pdf.pdf_validator import PDFValidator
SIGNATURE_FIELD_NAME = "Signature1"
class PyHankoPDFSigner(PDFSignerPort):
@ -20,6 +20,9 @@ class PyHankoPDFSigner(PDFSignerPort):
certificate: base64-encoded PKCS#12 (PFX)
"""
validator = PDFValidator()
validator.validate(pdf_bytes)
# 1⃣ Decodificar certificado
pfx_bytes = base64.b64decode(certificate)
@ -37,38 +40,41 @@ class PyHankoPDFSigner(PDFSignerPort):
cert = signer.signing_cert
subject = cert.subject.native.get('common_name', 'Desconocido')
# 5⃣ Define signature field position
# 5⃣ Define caja del campo de firma (AcroForm)
fields.append_signature_field(
writer,
sig_field_spec=fields.SigFieldSpec(
sig_field_name="Signature1",
sig_field_name=SIGNATURE_FIELD_NAME,
on_page=-1,
box=(380, 100, 560, 160), # x, y, x+width, y+height
box=(380, 130, 560, 170) # x, y, x+width, y+height
)
)
# 4⃣ Signature metadata (panel de firmas)
meta = signers.PdfSignatureMetadata(
field_name="Signature1",
field_name=SIGNATURE_FIELD_NAME,
name=subject,
)
pdf_signer = signers.PdfSigner(
meta, signer=signer, stamp_style=stamp.TextStampStyle(
stamp_text='Firmado digitalmente por: %(signer)s\nFecha: %(ts)s',
text_box_style=text.TextBoxStyle(
border_width=0,
font_size=12
#font=opentype.GlyphAccumulatorFactory('path to font police.ttf'),
meta,
signer=signer,
stamp_style=stamp.TextStampStyle(
stamp_text=(
"Firmado digitalmente por:\n"
"%(signer)s\n"
"Fecha: %(ts)s"
),
border_width=0, # 👈 ESTE es el borde que ves
background_opacity=0, # 👈 evita fondo gris/transparente
text_box_style=text.TextBoxStyle(
# borde interno (ya lo tenías bien)
border_width=0,
font_size=18,
),
#background=images.PdfImage('path to img.jpg')
)
)
)
# 4⃣ Firmar
signed_pdf: io.BytesIO = await pdf_signer.async_sign_pdf(
writer,

View File

@ -7,7 +7,7 @@ from dateutil import tz
from signing_service.application.settings.container import get_settings
from signing_service.application.settings.setup_logger import create_logger
from signing_service.api.routes.sign_document import router as sign_router
from signing_service.application.settings.version import get_package_version
from signing_service.application.settings.version import get_package_version
load_dotenv()
@ -23,7 +23,8 @@ log_dir = state_path
log_dir.mkdir(parents=True, exist_ok=True)
logger = create_logger(
name="factuges-document-signing-service",
log_path=log_dir / "factuges-document-signing-service.log", # Solo lo genera en producción
# Solo lo genera en producción
log_path=log_dir / "factuges-document-signing-service.log",
)
@ -42,10 +43,13 @@ logger.info("Infisical project ID: %s", settings.infisical_project_id)
logger.info("Infisical environment: %s", settings.infisical_env_slug)
logger.info("")
app = FastAPI(title="FactuGES Document Signing Service")
app.add_event_handler("startup", lambda: logger.info("Application startup complete"))
app.add_event_handler("shutdown", lambda: logger.info("Application shutdown complete"))
app.add_exception_handler(Exception, lambda request, exc: logger.error(f"Unhandled exception: {exc}"))
app = FastAPI(title="FactuGES Document Signing Service", version=version)
app.add_event_handler("startup", lambda: logger.info(
"Application startup complete"))
app.add_event_handler("shutdown", lambda: logger.info(
"Application shutdown complete"))
app.add_exception_handler(Exception, lambda request,
exc: logger.error(f"Unhandled exception: {exc}"))
# Register routers
@ -54,3 +58,6 @@ logger.info("API routes registered from sign_document router")
app.add_api_route("/health", lambda: {"status": "ok"}, methods=["GET"])
logger.info("Health check endpoint registered at /health")
app.add_api_route("/version", lambda: {"version": version}, methods=["GET"])
logger.info("Health check endpoint registered at /version")