diff --git a/app/main.py b/app/main.py index dd8c2a3..08cb6f1 100644 --- a/app/main.py +++ b/app/main.py @@ -431,7 +431,7 @@ async def litellm_create_key(email: str, budget: float) -> str: "key_alias": alias, "max_budget": budget, "budget_duration": "30d", - "models": ["deepseek-v4-flash", "gpt-oss-120b"], + "models": ["deepseek-v4-flash", "gpt-oss-120b", "glm-5-3-flash"], }, ) r.raise_for_status() @@ -785,6 +785,36 @@ async def admin_sync_loomio(token: str): return {"status": "synced"} +@app.post("/admin/provision/{token}") +async def admin_provision(token: str, request: Request): + """Manually provision a member by email (full pipeline). + + Protected by the same secret token as the webhook. Runs the same + provisioning as the OAuth callback: Cloudron user + members group + LiteLLM + key + invite email + Loomio sync. Body: {"email": "...", "name": "..."}. + Idempotent — safe to call repeatedly. + """ + if not WEBHOOK_TOKEN or not secrets.compare_digest(token, WEBHOOK_TOKEN): + raise HTTPException(401, "Invalid token") + + body = await request.json() + email = (body.get("email") or "").strip().lower() + name = body.get("name") or email.split("@")[0] + if not email: + raise HTTPException(400, "Missing email") + + user_id = await cloudron_create_user(email, name) + await cloudron_set_group(user_id) + await cloudron_set_active(user_id, True) + key_token = await litellm_create_key(email, MEMBER_BUDGET) + store_member(email, key_token, user_id, "") + await cloudron_send_invite(user_id, email) + await sync_loomio_memberships() + + logger.info("Manually provisioned member %s (user_id=%s)", email, user_id) + return {"status": "provisioned", "email": email, "user_id": user_id} + + @app.get("/") async def index(): return {"service": "Inference Cooperative Member Portal", "version": "0.1.0"}