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] [project]
name = "factuges-document-signing-service" name = "factuges-document-signing-service"
version = "0.1.3" version = "0.2.4"
description = "FastAPI service for signing PDF documents using external secret managers" description = "FastAPI service for signing PDF documents using external secret managers"
requires-python = ">=3.11" requires-python = ">=3.11"

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_VERSION="0.0.3" SCRIPT_VERSION="0.4"
# ===================================================== # =====================================================
# FACTUGES Document Signing Service Build Script # FACTUGES Document Signing Service Build Script
@ -64,9 +64,27 @@ if [[ $COMPANY =~ --.* ]]; then
exit 1 exit 1
fi fi
# --- Detectar nombre y versión de la API --- # --- Detectar nombre y versión ---
IMAGE_NAME=$(node -p "require('${PROJECT_DIR}/setup.cfg').name" 2>/dev/null || echo "factuges-document-signing-service") IMAGE_NAME=$(python - <<'EOF'
IMAGE_VERSION=$(node -p "require('${PROJECT_DIR}/setup.cfg').version" 2>/dev/null || echo "0.1.3") 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 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: def get_package_version() -> str:
try: try:
return version("factuges-document-signing-service") # nombre del paquete en [metadata].name return version("factuges-document-signing-service")
except PackageNotFoundError: except PackageNotFoundError:
return "unknown" 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 base64
import io import io
from datetime import datetime, timezone
from pyhanko import stamp from pyhanko import stamp
from pyhanko.sign import fields, signers from pyhanko.sign import fields, signers
from pyhanko.sign import signers
from pyhanko.pdf_utils import text from pyhanko.pdf_utils import text
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from signing_service.domain.ports.pdf_signer import PDFSignerPort 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): class PyHankoPDFSigner(PDFSignerPort):
@ -20,6 +20,9 @@ class PyHankoPDFSigner(PDFSignerPort):
certificate: base64-encoded PKCS#12 (PFX) certificate: base64-encoded PKCS#12 (PFX)
""" """
validator = PDFValidator()
validator.validate(pdf_bytes)
# 1⃣ Decodificar certificado # 1⃣ Decodificar certificado
pfx_bytes = base64.b64decode(certificate) pfx_bytes = base64.b64decode(certificate)
@ -37,38 +40,41 @@ class PyHankoPDFSigner(PDFSignerPort):
cert = signer.signing_cert cert = signer.signing_cert
subject = cert.subject.native.get('common_name', 'Desconocido') subject = cert.subject.native.get('common_name', 'Desconocido')
# 5⃣ Define caja del campo de firma (AcroForm)
# 5⃣ Define signature field position
fields.append_signature_field( fields.append_signature_field(
writer, writer,
sig_field_spec=fields.SigFieldSpec( sig_field_spec=fields.SigFieldSpec(
sig_field_name="Signature1", sig_field_name=SIGNATURE_FIELD_NAME,
on_page=-1, 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) # 4⃣ Signature metadata (panel de firmas)
meta = signers.PdfSignatureMetadata( meta = signers.PdfSignatureMetadata(
field_name="Signature1", field_name=SIGNATURE_FIELD_NAME,
name=subject, name=subject,
) )
pdf_signer = signers.PdfSigner( pdf_signer = signers.PdfSigner(
meta, signer=signer, stamp_style=stamp.TextStampStyle( meta,
stamp_text='Firmado digitalmente por: %(signer)s\nFecha: %(ts)s', signer=signer,
text_box_style=text.TextBoxStyle( stamp_style=stamp.TextStampStyle(
border_width=0, stamp_text=(
font_size=12 "Firmado digitalmente por:\n"
#font=opentype.GlyphAccumulatorFactory('path to font police.ttf'), "%(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 # 4⃣ Firmar
signed_pdf: io.BytesIO = await pdf_signer.async_sign_pdf( signed_pdf: io.BytesIO = await pdf_signer.async_sign_pdf(
writer, 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.container import get_settings
from signing_service.application.settings.setup_logger import create_logger 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.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() load_dotenv()
@ -23,7 +23,8 @@ log_dir = state_path
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
logger = create_logger( logger = create_logger(
name="factuges-document-signing-service", 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("Infisical environment: %s", settings.infisical_env_slug)
logger.info("") logger.info("")
app = FastAPI(title="FactuGES Document Signing Service") app = FastAPI(title="FactuGES Document Signing Service", version=version)
app.add_event_handler("startup", lambda: logger.info("Application startup complete")) app.add_event_handler("startup", lambda: logger.info(
app.add_event_handler("shutdown", lambda: logger.info("Application shutdown complete")) "Application startup complete"))
app.add_exception_handler(Exception, lambda request, exc: logger.error(f"Unhandled exception: {exc}")) 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 # 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"]) app.add_api_route("/health", lambda: {"status": "ok"}, methods=["GET"])
logger.info("Health check endpoint registered at /health") 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")