"""CLI support for creating user credential reset links. Usage (via main CLI): passkey-auth reset [query] If query is omitted, the master admin (first Administration role user in an organization granting auth:admin) is targeted. Otherwise query is matched as either an exact UUID or a case-insensitive substring of the display name. If multiple users match, they are listed and the command aborts. A new one-time reset link is always created. """ from __future__ import annotations import asyncio from uuid import UUID from passkey import authsession as _authsession from passkey import globals as _g from passkey.util import hostutil, passphrase from passkey.util import tokens as _tokens async def _resolve_targets(query: str | None): if query: # Try UUID targets: list[tuple] = [] try: q_uuid = UUID(query) perm_orgs = await _g.db.instance.get_permission_organizations("auth:admin") for o in perm_orgs: users = await _g.db.instance.get_organization_users(str(o.uuid)) for u, role_name in users: if u.uuid == q_uuid: return [(u, role_name)] # UUID not found among admin orgs -> fall back to substring search (rare case) except ValueError: pass # Substring search needle = query.lower() perm_orgs = await _g.db.instance.get_permission_organizations("auth:admin") for o in perm_orgs: users = await _g.db.instance.get_organization_users(str(o.uuid)) for u, role_name in users: if needle in (u.display_name or "").lower(): targets.append((u, role_name)) # De-duplicate seen = set() deduped = [] for u, role_name in targets: if u.uuid not in seen: seen.add(u.uuid) deduped.append((u, role_name)) return deduped # No query -> master admin perm_orgs = await _g.db.instance.get_permission_organizations("auth:admin") if not perm_orgs: return [] users = await _g.db.instance.get_organization_users(str(perm_orgs[0].uuid)) admin_users = [pair for pair in users if pair[1] == "Administration"] return admin_users[:1] async def _create_reset(user, role_name: str): token = passphrase.generate() await _g.db.instance.create_session( user_uuid=user.uuid, key=_tokens.reset_key(token), expires=_authsession.expires(), info={"type": "manual reset", "role": role_name}, ) return hostutil.reset_link_url(token), token async def _main(query: str | None) -> int: try: candidates = await _resolve_targets(query) if not candidates: print("No matching users found") return 1 if len(candidates) > 1: print("Multiple matches. Refine your query:") for u, role_name in candidates: print(f" - {u.display_name} ({u.uuid}) role={role_name}") return 2 user, role_name = candidates[0] link, token = await _create_reset(user, role_name) print(f"Reset link for {user.display_name} ({user.uuid}):\n{link}\n") return 0 except Exception as e: # pragma: no cover print("Failed to create reset link:", e) return 1 def run(query: str | None) -> int: """Synchronous wrapper for CLI entrypoint.""" return asyncio.run(_main(query)) __all__ = ["run"]