From b58a9755185b4d72c23918f2db76b163a3bcbe74 Mon Sep 17 00:00:00 2001 From: inference-bot Date: Sat, 5 Sep 2026 22:45:35 -0600 Subject: [PATCH] Gate OAuth on pending membership (payment check) + fix slug extraction for member.created events --- app/main.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index b8a8f87..766799b 100644 --- a/app/main.py +++ b/app/main.py @@ -120,6 +120,20 @@ def store_pending_member(slug: str, name: str) -> None: conn.close() +def is_pending_member(slug: str) -> bool: + """True if this slug has a pending membership (i.e. the webhook saw a + contribution from them). Used to gate the OAuth flow so only paying + members can provision an account.""" + if not slug: + return False + conn = get_db() + row = conn.execute( + "SELECT 1 FROM pending_members WHERE slug = ?", (slug,) + ).fetchone() + conn.close() + return row is not None + + def store_member(email: str, key_token: str, cloudron_user_id: str, slug: str = "") -> None: conn = get_db() conn.execute( @@ -323,12 +337,18 @@ async def opencollective_webhook(request: Request, token: str): event_type = payload.get("type", "") data = payload.get("data", {}) - member = data.get("member", {}) or data.get("fromCollective", {}) + # The webhook does NOT include email (Open Collective strips it for # privacy). We get name + slug, store a pending member, and obtain the # email later via the OAuth "connect your account" flow. - name = member.get("name", "Member") - slug = member.get("slug", "") + # + # Slug/name live in different places depending on the event type: + # - order.processed / transaction.created → data.fromCollective.{slug,name} + # - collective.member.created → data.member.memberCollective.{slug,name} + from_collective = data.get("fromCollective", {}) + member_collective = (data.get("member", {}) or {}).get("memberCollective", {}) + slug = from_collective.get("slug") or member_collective.get("slug", "") + name = from_collective.get("name") or member_collective.get("name", "Member") logger.info("Open Collective event: %s (name=%s, slug=%s)", event_type, name, slug) @@ -510,6 +530,33 @@ async def oauth_callback(request: Request): if not email: raise HTTPException(400, "No email returned — did you grant the email scope?") + # Gate: only provision if this person actually contributed (the webhook + # stored them as a pending member). Prevents free memberships via /join. + if not is_pending_member(slug): + logger.warning("Rejected non-member %s (slug=%s not in pending list)", email, slug) + html = """ + + +Not a member yet — Inference Cooperative + + + +
+

Not a member yet

+

We couldn't find an active membership for your account. To join, contribute on our Open Collective page first, then return here to finish setup.

+

Questions? Contact info@inference.coop.

+
+ +""" + return Response(content=html, media_type="text/html", status_code=403) + # Provision the member user_id = await cloudron_create_user(email, name) await cloudron_set_group(user_id)