From 3d93887e4e84e264acee60ae0311b2d21d0a2530 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 3 Feb 2026 16:23:25 +0100 Subject: [PATCH] ya firma bien --- LEEME.md => README.md | 0 pyproject.toml | 2 +- scripts/build-docker.sh | 26 +++++++++-- setup.cfg | 15 ------ .../application/settings/version.py | 2 +- .../infrastructure/pdf/__init__.py | 0 .../infrastructure/pdf/pdf_validator.py | 43 +++++++++++++++++ .../infrastructure/pdf/pdf_validator_error.py | 2 + .../infrastructure/pdf/pyhanko_pdf_signer.py | 46 +++++++++++-------- src/signing_service/main.py | 19 +++++--- 10 files changed, 108 insertions(+), 47 deletions(-) rename LEEME.md => README.md (100%) delete mode 100644 setup.cfg create mode 100644 src/signing_service/infrastructure/pdf/__init__.py create mode 100644 src/signing_service/infrastructure/pdf/pdf_validator.py create mode 100644 src/signing_service/infrastructure/pdf/pdf_validator_error.py diff --git a/LEEME.md b/README.md similarity index 100% rename from LEEME.md rename to README.md diff --git a/pyproject.toml b/pyproject.toml index 9d89934..5e39b9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/scripts/build-docker.sh b/scripts/build-docker.sh index 4fe2976..cb64610 100755 --- a/scripts/build-docker.sh +++ b/scripts/build-docker.sh @@ -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 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 351d655..0000000 --- a/setup.cfg +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/signing_service/application/settings/version.py b/src/signing_service/application/settings/version.py index cf93359..1d98517 100644 --- a/src/signing_service/application/settings/version.py +++ b/src/signing_service/application/settings/version.py @@ -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" diff --git a/src/signing_service/infrastructure/pdf/__init__.py b/src/signing_service/infrastructure/pdf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/signing_service/infrastructure/pdf/pdf_validator.py b/src/signing_service/infrastructure/pdf/pdf_validator.py new file mode 100644 index 0000000..9db5205 --- /dev/null +++ b/src/signing_service/infrastructure/pdf/pdf_validator.py @@ -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") """ diff --git a/src/signing_service/infrastructure/pdf/pdf_validator_error.py b/src/signing_service/infrastructure/pdf/pdf_validator_error.py new file mode 100644 index 0000000..f21e54c --- /dev/null +++ b/src/signing_service/infrastructure/pdf/pdf_validator_error.py @@ -0,0 +1,2 @@ +class PDFValidationError(Exception): + pass diff --git a/src/signing_service/infrastructure/pdf/pyhanko_pdf_signer.py b/src/signing_service/infrastructure/pdf/pyhanko_pdf_signer.py index 596db7d..33a298b 100644 --- a/src/signing_service/infrastructure/pdf/pyhanko_pdf_signer.py +++ b/src/signing_service/infrastructure/pdf/pyhanko_pdf_signer.py @@ -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, diff --git a/src/signing_service/main.py b/src/signing_service/main.py index 1c877e0..fa8b5c6 100644 --- a/src/signing_service/main.py +++ b/src/signing_service/main.py @@ -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")