# User management & disabling in-band registration (Prosody 0.11, verified 2026-08-29) Session-proven on the nixg.ru Prosody (Docker) — creates/verifies accounts, and the CORRECT way to shut off self-registration. ## Account operations (prosodyctl — the ONLY path once registration is off) ```bash # create (same command re-run = password change) docker exec icq-prosody prosodyctl register nixg.ru 'PASS' # delete docker exec icq-prosody prosodyctl unregister nixg.ru # list accounts (URL-encoded domain dir on host) ls /opt/icq/data/nixg%2eru/accounts/ # *.dat per account ``` `prosodyctl check accounts` is NOT a valid subcommand (0.11). Verify creation via the accounts dir, then by logging in. ## Disabling in-band self-registration — do BOTH, not just the flag ```lua modules_enabled = { -- comment the module OUT: -- "register"; ... } VirtualHost "nixg.ru" allow_registration = false -- AND flip the flag ``` - `allow_registration = false` alone leaves mod_register loaded and answering XEP-0077 (it just refuses) — but only unloading the module makes Prosody reply `service-unavailable`, which is the clean "registration closed" signal clients understand. - After edit: `docker exec icq-prosody prosodyctl check config` → "All checks passed", then `docker compose restart prosody`. ## Verification: raw XEP-0077 probe from a client Don't rely on client plugins (slixmpp's xep_0077 stanza may not be registered). Send a raw IQ with an existing authenticated session: ```python import asyncio, ssl import slixmpp async def main(): bot = slixmpp.ClientXMPP('user@nixg.ru', 'PASS') ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE bot.ssl_context = ctx result = asyncio.Event() async def on_start(ev): iq = bot.make_iq_set(sub=None, ito='nixg.ru') iq['id'] = 'regtest1' q = iq.xml.makeelement('{jabber:iq:register}query', {}) u = q.makeelement('username', {}); u.text = 'probeuser' p = q.makeelement('password', {}); p.text = 'ProbePass1' q.append(u); q.append(p); iq.xml.append(q) def cb(resp): print('REJECTED:', resp['type'], '/', resp['error']['condition']) result.set(); bot.disconnect() iq.send(callback=cb) bot.add_event_handler('session_start', on_start) await bot.connect(('localhost', 5222)) await asyncio.wait_for(result.wait(), timeout=15) asyncio.run(main()) ``` Expected with registration off: `REJECTED: error / service-unavailable` (no account created). With registration on: error `conflict` only if the username exists, otherwise success — so a probe also proves whether it is on. ## Verifying a login works (the reliable slixmpp pattern) `await bot.connect(...)` then waiting on `bot.disconnected.wait()` CRASHES (`'_asyncio.Future' object has no attribute 'wait'`). Use event handlers: ```python done = asyncio.Event() def on_session(ev): print('AUTH OK'); done.set() def on_fail(ev): print('AUTH FAILED'); done.set() bot.add_event_handler('session_start', on_session) bot.add_event_handler('failed_auth', on_fail) await bot.connect(('localhost', 5222)) await asyncio.wait_for(done.wait(), timeout=15) ``` `AUTH OK: session_start` = credentials valid. Works over c2s (localhost:5222) or `wss://xmpp.nixg.ru/xmpp-websocket` — good for smoke-testing new accounts and password resets.