Product is the only tenant table this service owns (porte table: organization_id -> product_id, everything below it -- tenant_ref/ branch_ref -- stays an opaque string owned by the consuming product, never a row here). Bcrypt-hashed API keys (passlib, same CryptContext shape as the auto's auth.service), generated once by scripts/create_product.py and never persisted in the clear. require_product (X-Api-Key -> Product, 401 on missing/wrong/soft-deleted) is the porte adaptation of the auto's require_permission — every /v1/* route will depend on it instead of a JWT bearer token. Also ports shared/base_model.py and shared/errors.py verbatim (Global Constraints: soft delete mixins, structured 409 bodies). products migration + real "alembic upgrade head" round-trip test (tests/migrations/, new shared _helpers.py instead of the auto's ad hoc cross-file _run_psql reuse). 13 tests green via `make k8s-test`.
51 lines
1.9 KiB
Python
51 lines
1.9 KiB
Python
"""CLI: provisions a new `Product` (a SowAI app consuming this service --
|
|
auto, crm, ...) and prints its PLAINTEXT API key exactly ONCE. Only the
|
|
bcrypt hash of the key is ever persisted (`tenancy.models.Product`'s
|
|
docstring) -- if the printed key is lost, the only recovery is provisioning
|
|
a new Product (or, once key rotation exists, rotating it; not built yet).
|
|
|
|
There is no HTTP endpoint for this on purpose (design spec, decision #5:
|
|
API keys are per-product and provisioning one is an operator action, not
|
|
self-service) -- this script is the ONLY writer of `products`.
|
|
|
|
Run against the target DB via the app's own session config (DATABASE_URL
|
|
env, same convention as `auto/backend/scripts/seed_dev_admin.py`):
|
|
|
|
uv run python scripts/create_product.py --name auto
|
|
"""
|
|
import argparse
|
|
import asyncio
|
|
import secrets
|
|
|
|
from fiscal_svc.core.db import async_session_maker
|
|
from fiscal_svc.tenancy.service import create_product
|
|
|
|
|
|
async def _main(name: str) -> None:
|
|
# `secrets.token_urlsafe` (CSPRNG, not `random`) -- this string IS the
|
|
# credential, same rigor as any password/token generated in this
|
|
# codebase. 32 bytes of entropy before urlsafe-base64 encoding.
|
|
api_key = secrets.token_urlsafe(32)
|
|
async with async_session_maker() as session:
|
|
product = await create_product(session, name=name, api_key=api_key)
|
|
|
|
print(f"Product criado: {product.name} (id={product.id})")
|
|
print()
|
|
print("API key -- copie agora. Só o hash bcrypt fica no banco; esta é a")
|
|
print("ÚNICA vez que o valor em claro é exibido:")
|
|
print()
|
|
print(api_key)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Provisiona um novo Product (consumidor da API do sowai-fiscal-svc)."
|
|
)
|
|
parser.add_argument("--name", required=True, help="Nome do produto SowAI (ex.: 'auto').")
|
|
args = parser.parse_args()
|
|
asyncio.run(_main(args.name))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|