diff --git a/README.md b/README.md index ba44046..ebec893 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ LibreChat ──→ Member Portal ──→ LiteLLM ──→ backend |----------|---------| | `CLOUDRON_API` | Cloudron API origin (auto-set by Cloudron) | | `CLOUDRON_TOKEN` | Cloudron API token (Read+Write) | +| `MEMBERS_GROUP_ID` | Cloudron group ID for members (grants chat access) | | `LITELLM_BASE` | LiteLLM gateway URL | | `LITELLM_MASTER_KEY` | LiteLLM master key | | `OPENCOLLECTIVE_WEBHOOK_SECRET` | Optional webhook signature secret | diff --git a/app/main.py b/app/main.py index dbd83ae..6e498bf 100644 --- a/app/main.py +++ b/app/main.py @@ -41,6 +41,10 @@ TIER_BUDGETS = { } DEFAULT_TIER = "member" +# The "members" group in Cloudron (group-based access control). +# Members are assigned to this group, which grants access to the chat app. +MEMBERS_GROUP_ID = os.environ.get("MEMBERS_GROUP_ID", "") + # --- Helpers --- def cloudron_headers() -> dict: @@ -48,11 +52,13 @@ def cloudron_headers() -> dict: async def cloudron_create_user(email: str, name: str) -> str: - """Create (or return existing) Cloudron user, assigned to the chat app. + """Create (or return existing) Cloudron user, assigned to the members group. Verified user shape (from live API): {id, username, email, fallbackEmail, displayName, role, active, groupIds} Roles: "owner", "admin", "user". + Group assignment is a SEPARATE call: PUT /api/v1/users/:userId/groups + with body {"groupIds": [...]}. """ async with httpx.AsyncClient() as client: # Check if user exists @@ -81,6 +87,20 @@ async def cloudron_create_user(email: str, name: str) -> str: return r.json()["id"] +async def cloudron_set_group(user_id: str) -> None: + """Assign a user to the members group (grants chat app access).""" + if not MEMBERS_GROUP_ID: + logger.warning("MEMBERS_GROUP_ID not set; skipping group assignment") + return + async with httpx.AsyncClient() as client: + r = await client.put( + f"{CLOUDRON_API}/api/v1/users/{user_id}/groups", + headers=cloudron_headers(), + json={"groupIds": [MEMBERS_GROUP_ID]}, + ) + r.raise_for_status() + + async def cloudron_set_active(user_id: str, active: bool) -> None: async with httpx.AsyncClient() as client: r = await client.post( @@ -163,6 +183,7 @@ async def opencollective_webhook(request: Request): tier = (data.get("tier") or {}).get("slug", DEFAULT_TIER) budget = TIER_BUDGETS.get(tier, TIER_BUDGETS[DEFAULT_TIER]) user_id = await cloudron_create_user(email, name) + await cloudron_set_group(user_id) await cloudron_set_active(user_id, True) await litellm_create_key(email, budget) return JSONResponse({"status": "activated", "user_id": user_id, "budget": budget})