ya firma bien
This commit is contained in:
parent
1a76b4aa22
commit
3d93887e4e
@ -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"
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
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:
|
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"
|
||||||
|
|||||||
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 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,
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user