ya firma bien
This commit is contained in:
parent
1a76b4aa22
commit
3d93887e4e
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
15
setup.cfg
15
setup.cfg
@ -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
|
||||
@ -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"
|
||||
|
||||
0
src/signing_service/infrastructure/pdf/__init__.py
Normal file
0
src/signing_service/infrastructure/pdf/__init__.py
Normal file
43
src/signing_service/infrastructure/pdf/pdf_validator.py
Normal file
43
src/signing_service/infrastructure/pdf/pdf_validator.py
Normal 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") """
|
||||
@ -0,0 +1,2 @@
|
||||
class PDFValidationError(Exception):
|
||||
pass
|
||||
@ -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,
|
||||
|
||||
@ -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")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user