diff --git a/README.md b/README.md index 8472b9d..ba44046 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,11 @@ endpoints: **Scaffold** — the three handlers are stubbed with the correct integration points. Remaining work before production: -- [ ] Verify Open Collective webhook event names + payload shape -- [ ] Verify Cloudron user-creation API payload (role/group assignment) +- [x] Verify Open Collective webhook event names + payload shape + → `order.processed` (every payment), `new member` (first only), `firstPayment` flag +- [x] Verify Cloudron user-creation API payload (role/group assignment) + → `POST /api/v1/users` with `{username, email, displayName, role, active}` - [ ] Add a persistent store (SQLite/Postgres) for email→key mapping - [ ] Add HMAC signature verification for the OC webhook - [ ] Wire the OIDC addon for admin access +- [ ] Verify LiteLLM key/generate + key/list payload shapes against live gateway diff --git a/app/main.py b/app/main.py index 0b2c138..dbd83ae 100644 --- a/app/main.py +++ b/app/main.py @@ -48,24 +48,34 @@ 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 chat app. + + Verified user shape (from live API): + {id, username, email, fallbackEmail, displayName, role, active, groupIds} + Roles: "owner", "admin", "user". + """ async with httpx.AsyncClient() as client: # Check if user exists r = await client.get( f"{CLOUDRON_API}/api/v1/users", headers=cloudron_headers(), - params={"email": email}, ) r.raise_for_status() - users = r.json().get("users", []) - if users: - return users[0]["id"] + for u in r.json().get("users", []): + if u.get("email") == email: + return u["id"] - # Create user + # Create user (role "user" = regular member) r = await client.post( f"{CLOUDRON_API}/api/v1/users", headers=cloudron_headers(), - json={"email": email, "displayName": name, "role": "user"}, + json={ + "username": email.split("@")[0], + "email": email, + "displayName": name, + "role": "user", + "active": True, + }, ) r.raise_for_status() return r.json()["id"] @@ -143,7 +153,12 @@ async def opencollective_webhook(request: Request): logger.info("Open Collective event: %s for %s", event_type, email) - if event_type in ("collective.member.created", "collective.transaction.created"): + # Open Collective webhook events (verified): + # - "order.processed" → fires on EVERY payment (incl. monthly recurring) + # - "new member" → fires on FIRST contribution only + # - payload has "firstPayment" boolean to distinguish new vs recurring + # - "collective.transaction.created" is DEPRECATED (being removed) + if event_type in ("order.processed", "new.member", "collective.member.created"): # New or renewed member → ensure active tier = (data.get("tier") or {}).get("slug", DEFAULT_TIER) budget = TIER_BUDGETS.get(tier, TIER_BUDGETS[DEFAULT_TIER])