Files
sowai-fiscal-svc/scripts/create_product.py
T
jonatanritter 1791435a8d feat: tenancy — products + API keys
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`.
2026-07-22 16:18:25 -03:00

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()