fix: JWT secret startup guard and order number advisory lock

H1: model_validator in Settings raises ValueError when jwt_secret_key is
    "changeme" and the process is running inside a container — fast-fail
    prevents insecure deployments; local dev outside Docker is unaffected.

H7: generate_order_number now acquires pg_advisory_xact_lock before SELECT MAX,
    matching the same pattern used in generate_invoice_number (billing/service.py).
    Concurrent order creation can no longer race to produce duplicate numbers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:45:39 +02:00
co-authored by Claude Sonnet 4.6
parent df304dc021
commit 3c2d0816e5
3 changed files with 21 additions and 3 deletions
+9
View File
@@ -83,6 +83,15 @@ class Settings(BaseSettings):
self.redis_url = _normalize_service_url(self.redis_url)
return self
@model_validator(mode="after")
def reject_insecure_jwt_secret_in_production(self) -> "Settings":
if self.jwt_secret_key == "changeme" and _is_running_in_container():
raise ValueError(
"JWT_SECRET_KEY must be set to a secure random value in production. "
"The default 'changeme' key is not permitted when running inside a container."
)
return self
# JWT
jwt_secret_key: str = "changeme"
jwt_algorithm: str = "HS256"
+6 -3
View File
@@ -15,12 +15,15 @@ def _utcnow_naive() -> datetime:
async def generate_order_number(db: AsyncSession) -> str:
"""Generate next sequential order number: SA-2026-XXXXX."""
"""Generate next sequential order number: SA-YYYY-XXXXX."""
from sqlalchemy import text
year = datetime.now(timezone.utc).year
prefix = f"SA-{year}-"
# Use MAX to find the highest existing sequence number this year.
# COUNT-based approach breaks when orders are deleted (produces duplicates).
# Advisory lock prevents duplicate numbers under concurrent order creation.
# Released automatically when the surrounding transaction commits or rolls back.
await db.execute(text("SELECT pg_advisory_xact_lock(hashtext(:key))"), {"key": f"order_number_seq_{year}"})
result = await db.execute(
select(func.max(Order.order_number)).where(Order.order_number.like(f"{prefix}%"))
)