diff --git a/app/main.py b/app/main.py index c3a736b..9097503 100644 --- a/app/main.py +++ b/app/main.py @@ -38,7 +38,6 @@ CLOUDRON_API = os.environ.get("CLOUDRON_API_ORIGIN", "https://my.inference.coop" CLOUDRON_TOKEN = os.environ.get("CLOUDRON_TOKEN", "") LITELLM_BASE = os.environ.get("LITELLM_BASE", "https://gateway.inference.coop") LITELLM_MASTER_KEY = os.environ.get("LITELLM_MASTER_KEY", "") -OPENCOLLECTIVE_SECRET = os.environ.get("OPENCOLLECTIVE_WEBHOOK_SECRET", "") # Shared secret that the chat frontend uses to authenticate requests to the # portal. LibreChat sends it as an X-Portal-Secret header; OpenWebUI signs the @@ -425,21 +424,6 @@ def store_member(email: str, key_token: str, cloudron_user_id: str, slug: str = conn.close() -def rekey_member(email: str, key_token: str) -> None: - """Update only the key token, preserving cloudron_user_id and slug. - - Used by the Teams migration to swap in a fresh sk- key without clobbering - the member's existing Cloudron identity. - """ - conn = get_db() - conn.execute( - "UPDATE members SET key_token = ?, active = 1 WHERE email = ?", - (key_token, email), - ) - conn.commit() - conn.close() - - def get_member_key(email: str) -> str | None: conn = get_db() row = conn.execute( @@ -833,8 +817,10 @@ async def opencollective_webhook(request: Request, token: str): # New member → provision immediately and send our own welcome email. # The webhook strips email, so look it up by slug via the admin token. # Only provision on firstPayment (order.processed also fires on monthly - # renewals, which should NOT re-send the welcome email). - first_payment = data.get("firstPayment", True) + # renewals, which should NOT re-send the welcome email). Default to + # False so a missing field never triggers a spurious re-provision; the + # nightly sweep backstops any missed first payment. + first_payment = data.get("firstPayment", False) if slug and first_payment: members = await fetch_members() for m in members: @@ -1052,41 +1038,6 @@ async def admin_set_balance(request: Request): return {"status": "updated", "email": email, "balance": new_balance, "team_budget": applied} -@app.post("/admin/migrate-teams") -async def admin_migrate_teams(request: Request): - """One-off migration: put every active member under a LiteLLM team + fresh - sk- key. Fixes a bug where some members had SHA256 hashes (from /key/list) - stored as their key, which cannot authenticate. - - For each active member: delete any existing key(s) by alias, create/reuse a - team, generate a fresh sk- key under it, and store the sk- token. Idempotent - but re-issues keys, so call once. - """ - _verify_admin_token(request) - - conn = get_db() - rows = conn.execute("SELECT email, key_token FROM members WHERE active = 1").fetchall() - conn.close() - - results = [] - for email, old_token in rows: - try: - # Delete any existing key(s) for this alias (by hash or sk-). - await litellm_delete_keys_by_alias(email) - # (Re)create the team and a fresh sk- key, at the member's balance. - balance = get_member_balance(email) - team_id = await litellm_get_or_create_team(email, balance) - new_key = await litellm_create_key(email, balance, team_id) - rekey_member(email, new_key) # preserve cloudron_user_id + slug - results.append({"email": email, "status": "rekeyed"}) - logger.info("Migrated %s to team %s", email, team_id) - except Exception as e: - results.append({"email": email, "status": "error", "error": str(e)}) - logger.warning("Migrate failed for %s: %s", email, e) - - return {"status": "migrated", "results": results} - - @app.get("/admin/overview") async def admin_overview(request: Request): """Return a co-op-wide overview for the admin dashboard. @@ -1168,39 +1119,6 @@ async def admin_overview(request: Request): } -async def litellm_delete_keys_by_alias(email: str) -> None: - """Delete all LiteLLM keys whose alias matches member:. - - /key/list returns SHA256 hashes; /key/delete accepts hashes. This clears - both legacy hash-keyed and current sk- keyed entries for a member. - """ - alias = f"member:{email}" - async with httpx.AsyncClient() as client: - r = await client.get( - f"{LITELLM_BASE}/key/list", - headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, - ) - r.raise_for_status() - hashes = r.json().get("keys", []) - to_delete = [] - for h in hashes: - info = await client.get( - f"{LITELLM_BASE}/key/info", - headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, - params={"key": h}, - ) - if info.status_code == 200: - data = info.json().get("info", {}) - if data.get("key_alias") == alias: - to_delete.append(h) - if to_delete: - await client.post( - f"{LITELLM_BASE}/key/delete", - headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}, - json={"keys": to_delete}, - ) - - # --- Broker HTTP endpoints (called by the member dashboard) --- # The dashboard is Cloudron-SSO-gated; it authenticates the member and passes # their email with a shared secret. The portal trusts the email only because it