diff --git a/app/main.py b/app/main.py index 3e16b4e..e4b9849 100644 --- a/app/main.py +++ b/app/main.py @@ -56,15 +56,6 @@ OWUI_JWT_SECRET = os.environ.get("OWUI_JWT_SECRET", "") # is the standard way to authenticate them. WEBHOOK_TOKEN = os.environ.get("WEBHOOK_TOKEN", "") -# Open Collective OAuth app credentials (for the "connect your account" flow, -# which is how we obtain a member's email — the webhook strips it for privacy). -OC_OAUTH_CLIENT_ID = os.environ.get("OC_OAUTH_CLIENT_ID", "") -OC_OAUTH_CLIENT_SECRET = os.environ.get("OC_OAUTH_CLIENT_SECRET", "") -OC_OAUTH_REDIRECT_URI = os.environ.get( - "OC_OAUTH_REDIRECT_URI", "https://portal.inference.coop/oauth/callback" -) -OC_AUTHORIZE_URL = "https://opencollective.com/oauth/authorize" -OC_TOKEN_URL = "https://opencollective.com/oauth/token" OC_GRAPHQL_URL = "https://opencollective.com/api/graphql/v2" OC_COLLECTIVE_SLUG = os.environ.get("OC_COLLECTIVE_SLUG", "inference-cooperative") @@ -138,13 +129,6 @@ def get_db() -> sqlite3.Connection: "active INTEGER DEFAULT 1" ")" ) - conn.execute( - "CREATE TABLE IF NOT EXISTS pending_members (" - "slug TEXT PRIMARY KEY, " - "name TEXT, " - "created_at TEXT DEFAULT (datetime('now'))" - ")" - ) # Migration: add slug column if the members table predates it. cols = [r[1] for r in conn.execute("PRAGMA table_info(members)").fetchall()] if "slug" not in cols: @@ -152,81 +136,6 @@ def get_db() -> sqlite3.Connection: return conn -def store_pending_member(slug: str, name: str) -> None: - conn = get_db() - conn.execute( - "INSERT INTO pending_members (slug, name) VALUES (?, ?) " - "ON CONFLICT(slug) DO UPDATE SET name=excluded.name", - (slug, name), - ) - conn.commit() - 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 - - -async def fetch_member_emails() -> dict[str, str]: - """Return a map of email -> role for active financial contributors. - - Uses the bot's personal token (admin) so the GraphQL API exposes member - emails, which are hidden from anonymous access. This is the authoritative - source of truth for "who is a member" — and it works for guest - contributors (who have no OC account) because their email is still in the - list. - """ - if not OC_PERSONAL_TOKEN: - logger.warning("OC_PERSONAL_TOKEN not set; cannot fetch member emails") - return {} - query = ( - '{ collective(slug: "%s") { members(limit: 100) { nodes { role account { email } } } } }' - % OC_COLLECTIVE_SLUG - ) - async with httpx.AsyncClient() as client: - r = await client.post( - OC_GRAPHQL_URL, - headers={"Personal-Token": OC_PERSONAL_TOKEN}, - json={"query": query}, - ) - if r.status_code != 200: - logger.warning("OC member fetch failed: %s", r.status_code) - return {} - nodes = r.json().get("data", {}).get("collective", {}).get("members", {}).get("nodes", []) - result: dict[str, str] = {} - for n in nodes: - role = n.get("role", "") - acct = n.get("account", {}) or {} - email = (acct.get("email") or "").strip().lower() - if email and role in ("BACKER", "ADMIN"): - result[email] = role - return result - - -async def is_active_member(email: str) -> bool: - """Check whether this email is an active financial contributor. - - Matches on email (not slug) so guest contributors — who have no OC account - and thus no usable slug — are still recognized. The email comes from the - OAuth `me` query (with the user's consent); we match it against the - admin-visible member list. - """ - if not email: - return False - members = await fetch_member_emails() - return email.strip().lower() in members - - async def fetch_members() -> list[dict]: """Return the full active-member list: email, name, slug, role. @@ -380,11 +289,8 @@ def welcome_email(name: str, setup_url: str) -> tuple[str, str, str]: async def get_active_member_emails() -> list[str]: """Return the list of active member emails from the portal's own database. - We use the local `members` table (populated during OAuth, which captures the - member's email) rather than Open Collective's GraphQL, because OC returns - `email: null` for privacy — the email field is only exposed via OAuth with - the user's consent. The local table is the authoritative source of member - emails. + The local `members` table is the authoritative source of member emails + (populated during provisioning). Used for the Loomio sync. """ conn = get_db() rows = conn.execute( @@ -541,17 +447,6 @@ async def cloudron_set_active(user_id: str, active: bool) -> None: r.raise_for_status() -async def cloudron_send_invite(user_id: str, email: str) -> None: - """Send the account-setup invite email to the member.""" - async with httpx.AsyncClient() as client: - r = await client.post( - f"{CLOUDRON_API}/api/v1/users/{user_id}/send_invite_email", - headers=cloudron_headers(), - json={"email": email}, - ) - r.raise_for_status() - - async def cloudron_get_invite_link(user_id: str) -> str: """Return the account-setup link WITHOUT sending Cloudron's own email. @@ -674,8 +569,7 @@ async def opencollective_webhook(request: Request, token: str): data = payload.get("data", {}) # 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. + # privacy). We get name + slug, then look up the email via the admin token. # # Slug/name live in different places depending on the event type: # - order.processed / transaction.created → data.fromCollective.{slug,name} @@ -819,157 +713,6 @@ async def inject_key(request: Request, path: str): ) -# --- C. OAuth "connect your account" flow --- - -@app.get("/join") -async def join_page(): - """The 'finish your setup' page — where a new member connects their - Open Collective account so we can obtain their email (with consent).""" - html = """ - -
- - -Thanks for joining the Inference Cooperative! To set up your account, connect your Open Collective account so we can verify your membership.
- Connect Open Collective -You'll receive an account-setup email shortly after connecting.
Didn't get it? Contact info@inference.coop.
Contributed as a guest? Create an Open Collective account using the same email you used to contribute, then return here and connect it.
-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.
-Contributed as a guest? Make sure you've created an Open Collective account with the same email you used to contribute, then try again.
-Questions? Contact info@inference.coop.
-Your account is being set up. Check your email for a link to create your account and start chatting.
-Didn't get it? Contact info@inference.coop.
-