Loomio: fix auto-generated usernames after sync (b3 server API; prefer Cloudron uid, fall back to email local-part)
This commit is contained in:
1 parent
070c6525cd
commit
ef253d01a0
1 file changed
+60
+60
@@ -170,6 +170,11 @@ MANUAL_MEMBERS = {
|
|||||||
# group to the current active-member list via the User API (/api/b2).
|
# group to the current active-member list via the User API (/api/b2).
|
||||||
LOOMIO_BASE = os.environ.get("LOOMIO_BASE", "https://forum.inference.coop")
|
LOOMIO_BASE = os.environ.get("LOOMIO_BASE", "https://forum.inference.coop")
|
||||||
LOOMIO_API_KEY = os.environ.get("LOOMIO_API_KEY", "")
|
LOOMIO_API_KEY = os.environ.get("LOOMIO_API_KEY", "")
|
||||||
|
# Server API key (B3) — used to fix auto-generated Loomio usernames after
|
||||||
|
# invites. Loomio's b2/memberships API only takes emails; new users get a
|
||||||
|
# mangled auto-handle (email local-part + random suffix). The B3 users API
|
||||||
|
# lets us rename them to the member's Cloudron username.
|
||||||
|
LOOMIO_B3_KEY = os.environ.get("LOOMIO_B3_KEY", "")
|
||||||
LOOMIO_GROUP_ID = os.environ.get("LOOMIO_GROUP_ID", "")
|
LOOMIO_GROUP_ID = os.environ.get("LOOMIO_GROUP_ID", "")
|
||||||
# Operator accounts that must always remain in the Loomio group (never removed
|
# Operator accounts that must always remain in the Loomio group (never removed
|
||||||
# by the remove_absent reconciliation), comma-separated.
|
# by the remove_absent reconciliation), comma-separated.
|
||||||
@@ -772,6 +777,51 @@ async def get_active_member_emails() -> list[str]:
|
|||||||
return [r[0] for r in rows]
|
return [r[0] for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict:
|
||||||
|
"""Rename Loomio users with auto-generated handles to their Cloudron username.
|
||||||
|
|
||||||
|
Loomio's b2/memberships invite creates the account with a mangled handle
|
||||||
|
(email local-part + random suffix, e.g. danishipleydsdk4ibdihka). After
|
||||||
|
first SSO login the user carries an oauth identity whose uid IS their
|
||||||
|
Cloudron username — prefer that; fall back to the email local-part.
|
||||||
|
Uses the server (b3) API. Returns a {fixed, skipped} summary.
|
||||||
|
"""
|
||||||
|
H = {"Authorization": f"Bearer {LOOMIO_B3_KEY}", "Content-Type": "application/json"}
|
||||||
|
r = await client.get(f"{LOOMIO_BASE}/api/b3/users", headers=H)
|
||||||
|
r.raise_for_status()
|
||||||
|
users = r.json().get("users", [])
|
||||||
|
|
||||||
|
fixed, skipped = 0, 0
|
||||||
|
for u in users:
|
||||||
|
target = None
|
||||||
|
for ident in u.get("identities") or []:
|
||||||
|
if ident.get("identity_type") == "oauth" and ident.get("uid"):
|
||||||
|
uid = ident["uid"].lower()
|
||||||
|
# Loomio usernames: lowercase letters, numbers, underscores only.
|
||||||
|
if uid.replace("_", "").isalnum() and len(uid) >= 2:
|
||||||
|
target = uid
|
||||||
|
break
|
||||||
|
target = (u.get("email") or "").split("@")[0].lower() or None
|
||||||
|
break
|
||||||
|
if not target and u.get("email") and "@" in u["email"]:
|
||||||
|
target = u["email"].split("@")[0].lower()
|
||||||
|
if not target or u.get("username") == target:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
rr = await client.patch(
|
||||||
|
f"{LOOMIO_BASE}/api/b3/users/{u['id']}",
|
||||||
|
headers=H, json={"username": target},
|
||||||
|
)
|
||||||
|
if rr.status_code == 200:
|
||||||
|
fixed += 1
|
||||||
|
else:
|
||||||
|
logger.warning("Loomio username fix for %s failed: %s %s",
|
||||||
|
u.get("email"), rr.status_code, rr.text[:120])
|
||||||
|
if fixed:
|
||||||
|
logger.info("Loomio usernames fixed: %s", fixed)
|
||||||
|
return {"fixed": fixed, "skipped": skipped}
|
||||||
|
|
||||||
|
|
||||||
async def sync_loomio_memberships() -> None:
|
async def sync_loomio_memberships() -> None:
|
||||||
"""Reconcile the Loomio group to the current active-member list.
|
"""Reconcile the Loomio group to the current active-member list.
|
||||||
|
|
||||||
@@ -805,6 +855,16 @@ async def sync_loomio_memberships() -> None:
|
|||||||
result.get("removed_emails", []),
|
result.get("removed_emails", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fix auto-generated usernames (email+randomsuffix) → Cloudron username.
|
||||||
|
# Runs after every sync so newly invited members get corrected within a day
|
||||||
|
# (or immediately when the sync follows provisioning). Best-effort: no B3
|
||||||
|
# key or API failure just logs and moves on.
|
||||||
|
if LOOMIO_B3_KEY:
|
||||||
|
try:
|
||||||
|
await loomio_fix_usernames(client)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Loomio username fix pass failed: %s", e)
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
Reference in new issue
Block a user