Gate OAuth on pending membership (payment check) + fix slug extraction for member.created events
This commit is contained in:
1 parent
e291406b0b
commit
b58a975518
1 file changed
+50
-3
+50
-3
@@ -120,6 +120,20 @@ def store_pending_member(slug: str, name: str) -> None:
|
|||||||
conn.close()
|
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:
|
def store_member(email: str, key_token: str, cloudron_user_id: str, slug: str = "") -> None:
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -323,12 +337,18 @@ async def opencollective_webhook(request: Request, token: str):
|
|||||||
|
|
||||||
event_type = payload.get("type", "")
|
event_type = payload.get("type", "")
|
||||||
data = payload.get("data", {})
|
data = payload.get("data", {})
|
||||||
member = data.get("member", {}) or data.get("fromCollective", {})
|
|
||||||
# The webhook does NOT include email (Open Collective strips it for
|
# The webhook does NOT include email (Open Collective strips it for
|
||||||
# privacy). We get name + slug, store a pending member, and obtain the
|
# privacy). We get name + slug, store a pending member, and obtain the
|
||||||
# email later via the OAuth "connect your account" flow.
|
# 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)
|
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:
|
if not email:
|
||||||
raise HTTPException(400, "No email returned — did you grant the email scope?")
|
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 = """<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Not a member yet — Inference Cooperative</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; background: #faf8f5; color: #2d3327; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; padding: 24px; }
|
||||||
|
.card { background: #fff; border: 1px solid #e8e2d8; border-radius: 12px; padding: 40px; max-width: 480px; text-align: center; }
|
||||||
|
h1 { font-size: 24px; margin: 0 0 12px; }
|
||||||
|
p { color: #6b7a62; line-height: 1.5; margin: 0 0 20px; }
|
||||||
|
.note { font-size: 13px; color: #94a08c; margin-top: 20px; }
|
||||||
|
.note a { color: #6b7a62; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Not a member yet</h1>
|
||||||
|
<p>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.</p>
|
||||||
|
<p class="note">Questions? Contact <a href="mailto:info@inference.coop">info@inference.coop</a>.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
return Response(content=html, media_type="text/html", status_code=403)
|
||||||
|
|
||||||
# Provision the member
|
# Provision the member
|
||||||
user_id = await cloudron_create_user(email, name)
|
user_id = await cloudron_create_user(email, name)
|
||||||
await cloudron_set_group(user_id)
|
await cloudron_set_group(user_id)
|
||||||
|
|||||||
Reference in new issue
Block a user