diff --git a/app/main.py b/app/main.py index 4295feb..657ea4d 100644 --- a/app/main.py +++ b/app/main.py @@ -157,7 +157,7 @@ async def fetch_members() -> list[dict]: logger.warning("OC_PERSONAL_TOKEN not set; cannot fetch members") return [] query = ( - '{ collective(slug: "%s") { members(limit: 100) { nodes { role account { email name slug } } } } }' + '{ collective(slug: "%s") { members(limit: 100) { nodes { role account { name slug ... on Individual { email } } } } } }' % OC_COLLECTIVE_SLUG ) async with httpx.AsyncClient() as client: @@ -783,6 +783,13 @@ async def reconcile_memberships() -> dict: active = await fetch_members() active_emails = {m["email"] for m in active} + # FAIL-SAFE: if the OC fetch returned nothing (token expired, OC outage, + # or a query error), we must NOT deactivate everyone — that would be a + # catastrophic false-positive. Skip reconciliation entirely in that case. + if not active: + logger.warning("Sweep: fetch_members returned empty; skipping reconciliation (fail-safe)") + return {"status": "skipped", "reason": "empty_member_list", "provisioned": 0, "deactivated": 0} + # Currently provisioned members (from our DB). conn = get_db() rows = conn.execute("SELECT email, cloudron_user_id FROM members WHERE active = 1").fetchall()