This commit is contained in:
David Arranz 2026-01-22 13:07:15 +01:00
parent 76103ac0f2
commit d57355a9c9
3 changed files with 102 additions and 43 deletions

View File

@ -13,7 +13,6 @@ dependencies = [
"python-dotenv>=1.0", "python-dotenv>=1.0",
"fastapi>=0.110", "fastapi>=0.110",
"uvicorn[standard]>=0.27", "uvicorn[standard]>=0.27",
"pydantic-settings>=2.2",
"pyhanko>=0.25", "pyhanko>=0.25",
"cryptography>=42", "cryptography>=42",

View File

@ -1,54 +1,114 @@
from pydantic_settings import BaseSettings import os
from pydantic import Field, model_validator
class AppSettings(BaseSettings): class AppSettings:
def __init__(self) -> None:
# -------------------- # --------------------
# Application # Application
# -------------------- # --------------------
app_env: str = Field(default="local") self.app_env = self._get_env(
log_level: str = Field(default="INFO") "APP_ENV",
default="local",
normalize=self._normalize_lower,
)
self.log_level = self._get_env(
"LOG_LEVEL",
default="INFO",
normalize=self._normalize_upper,
)
# -------------------- # --------------------
# Secret manager # Secret manager
# -------------------- # --------------------
secret_provider: str = Field(default="fake") self.secret_provider = self._get_env(
gcp_project_id: str | None = None "SECRET_PROVIDER",
default="fake",
normalize=self._normalize_lower,
)
self.gcp_project_id = self._get_env(
"GCP_PROJECT_ID",
default=None,
required=self.secret_provider == "google",
)
# -------------------- # --------------------
# PDF signing # PDF signing
# -------------------- # --------------------
pdf_cert_secret_name: str self.pdf_cert_secret_name = self._get_env(
pdf_cert_password_secret_name: str "PDF_CERT_SECRET_NAME",
required=True,
@model_validator(mode="after")
def validate_required_settings(self) -> "AppSettings":
"""
Fail fast if required configuration is missing.
"""
if not self.pdf_cert_secret_name:
raise ValueError("PDF_CERT_SECRET_NAME is required")
if not self.pdf_cert_password_secret_name:
raise ValueError("PDF_CERT_PASSWORD_SECRET_NAME is required")
# ---- Secret provider validation ----
if self.secret_provider == "google":
if not self.gcp_project_id:
raise ValueError(
"GCP_PROJECT_ID is required when SECRET_PROVIDER=google"
) )
# ---- PDF signing validation ---- self.pdf_cert_password_secret_name = self._get_env(
if self.app_env == "prod": "PDF_CERT_PASSWORD_SECRET_NAME",
if not self.pdf_pfx_password: required=True,
raise ValueError(
"PDF_PFX_PASSWORD is required in production"
) )
return self # --------------------
# Cross-field validation
# --------------------
self._validate()
class Config: # ======================================================
env_file = ".env" # Helpers
case_sensitive = False # ======================================================
def _get_env(
self,
name: str,
*,
default: str | None = None,
required: bool = False,
normalize=None,
) -> str | None:
"""
Read and validate an environment variable.
"""
raw = os.environ.get(name, default)
if raw is None:
if required:
raise ValueError(f"Environment variable {name} is required")
return None
if not isinstance(raw, str):
raise ValueError(f"Environment variable {name} must be a string")
value = raw.strip()
if required and value == "":
raise ValueError(f"Environment variable {name} cannot be empty")
if normalize:
value = normalize(value)
return value
@staticmethod
def _normalize_lower(value: str) -> str:
return value.strip().lower()
@staticmethod
def _normalize_upper(value: str) -> str:
return value.strip().upper()
# ======================================================
# Validations
# ======================================================
def _validate(self) -> None:
allowed_providers = {"fake", "infisical", "google"}
if self.secret_provider not in allowed_providers:
raise ValueError(
f"Invalid SECRET_PROVIDER: {self.secret_provider}. "
f"Allowed values: {allowed_providers}"
)
if self.app_env not in {"local", "prod"}:
raise ValueError(
f"Invalid APP_ENV: {self.app_env}. "
"Allowed values: local | prod"
)

View File

@ -10,7 +10,7 @@ load_dotenv()
# 👇 FAIL FAST: load settings at startup # 👇 FAIL FAST: load settings at startup
get_settings() get_settings()
app = FastAPI(title="Signing Service") app = FastAPI(title="FactuGES Document Signing Service")
app.include_router(sign_router) app.include_router(sign_router)
@app.get("/health") @app.get("/health")