Gate on live OC membership list (authoritative) instead of fragile webhook pending table
This commit is contained in:
1 parent
b58a975518
commit
5c28176d30
1 file changed
+31
-4
+31
-4
@@ -55,6 +55,7 @@ OC_OAUTH_REDIRECT_URI = os.environ.get(
|
|||||||
OC_AUTHORIZE_URL = "https://opencollective.com/oauth/authorize"
|
OC_AUTHORIZE_URL = "https://opencollective.com/oauth/authorize"
|
||||||
OC_TOKEN_URL = "https://opencollective.com/oauth/token"
|
OC_TOKEN_URL = "https://opencollective.com/oauth/token"
|
||||||
OC_GRAPHQL_URL = "https://opencollective.com/api/graphql/v2"
|
OC_GRAPHQL_URL = "https://opencollective.com/api/graphql/v2"
|
||||||
|
OC_COLLECTIVE_SLUG = os.environ.get("OC_COLLECTIVE_SLUG", "inference-cooperative")
|
||||||
|
|
||||||
# Monthly credit budget (in USD of tokens) for all members.
|
# Monthly credit budget (in USD of tokens) for all members.
|
||||||
# Single sliding-scale tier: everyone gets the same $15/month in credits,
|
# Single sliding-scale tier: everyone gets the same $15/month in credits,
|
||||||
@@ -134,6 +135,31 @@ def is_pending_member(slug: str) -> bool:
|
|||||||
return row is not None
|
return row is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def is_active_member(slug: str) -> bool:
|
||||||
|
"""Check the LIVE Open Collective membership list (authoritative source of
|
||||||
|
truth) for whether this slug is a financial contributor (BACKER/ADMIN),
|
||||||
|
not just the fiscal host. More reliable than the webhook, which only fires
|
||||||
|
on new events and can miss existing members."""
|
||||||
|
if not slug:
|
||||||
|
return False
|
||||||
|
query = (
|
||||||
|
'{ collective(slug: "%s") { members(limit: 100) { nodes { role account { slug } } } } }'
|
||||||
|
% OC_COLLECTIVE_SLUG
|
||||||
|
)
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.post(OC_GRAPHQL_URL, json={"query": query})
|
||||||
|
if r.status_code != 200:
|
||||||
|
logger.warning("OC membership check failed: %s", r.status_code)
|
||||||
|
return False
|
||||||
|
nodes = r.json().get("data", {}).get("collective", {}).get("members", {}).get("nodes", [])
|
||||||
|
for n in nodes:
|
||||||
|
role = n.get("role", "")
|
||||||
|
acct = n.get("account", {}) or {}
|
||||||
|
if acct.get("slug") == slug and role in ("BACKER", "ADMIN"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
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(
|
||||||
@@ -530,10 +556,11 @@ 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
|
# Gate: only provision if this person is an active financial contributor.
|
||||||
# stored them as a pending member). Prevents free memberships via /join.
|
# We check the LIVE Open Collective membership list (authoritative), not
|
||||||
if not is_pending_member(slug):
|
# the webhook's pending table (which only fires on new events).
|
||||||
logger.warning("Rejected non-member %s (slug=%s not in pending list)", email, slug)
|
if not await is_active_member(slug):
|
||||||
|
logger.warning("Rejected non-member %s (slug=%s not an active contributor)", email, slug)
|
||||||
html = """<!DOCTYPE html>
|
html = """<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|||||||
Reference in new issue
Block a user