Admin accounts¶
The admin web console supports real accounts — a username and password
per human, with a revocable server-side session — alongside the legacy
API-key login. An account is only a way to authenticate; authorization
is read from the existing Role/Team system, keyed by
the account's linked Telegram user_id. There is no second permission
model.
Why¶
Previously the only desktop login was pasting an API key, and any valid key was treated as full admin. That meant no per-person identity, no audit trail, and no way to revoke one person without breaking the shared key. Real accounts fix all three: each login is a named human, bound to a Telegram identity whose Role governs what they can do.
The three auth paths¶
The API accepts, in this precedence order:
- Telegram Mini-App
initData— ordinary end users in the Mini-App. - Admin session cookie — a logged-in
AdminAccount(this feature). Authorization: BearerAPI key — legacy / scripts / webhooks.
A logged-in account's permissions come from
resolve_role(telegram_user_id) (ADMIN_IDS-aware). AGENT and above
may open the console; finer per-route checks map each API scope to a
minimum Role.
Inviting a new admin¶
Registration is invite-only — it consumes a single-use registration key minted by an existing admin from the bot. The key is bound to the invitee's Telegram identity (not the issuer's), so the new account inherits that person's Role.
From an admin DM with the bot:
/apikey invite <telegram_user_id> [label]
…or run /apikey invite while forwarding a message from the new admin
(the bot reads their id from the forward; if their privacy hides it, fall
back to the numeric id). The same flow is available as the 🎟 Invite
admin button in the /apikey menu.
The bot replies once with the invite key. Hand it to the new admin — they
register with it at the console's "Create your account" screen
(/register). On success the key is permanently burned: it can never
register a second account, nor be used as a bearer token.
Registration keys carry no API scopes — they exist only to create one account and are dead the moment they're redeemed.
Bootstrapping a fresh deployment¶
Anyone in ADMIN_IDS can run /apikey invite immediately — bot command
access is gated on ADMIN_IDS directly, independent of the roles
collection, and resolve_role treats ADMIN_IDS members as at least
Role.ADMIN. So the very first admin invites themselves
(/apikey invite <their own id>), registers, and is in.
Logging in¶
The console's primary form is username + password (POST /api/v1/auth/login),
which sets the session cookie. The legacy "Log in with an API key
instead" path is still present, demoted below the primary form, and works
exactly as before — existing deployments are never locked out.
Managing accounts¶
Owners/admins get an Accounts section in the console (and
GET /api/v1/auth/accounts) listing every account with its resolved Role
and last login. Disable soft-deactivates an account and immediately
revokes all of its live sessions (it stops working on the very next
request, not at session expiry). Enable reverses it. There is no
deletion — disable is reversible and sufficient.
A disabled account learns nothing at the login screen: unknown username, wrong password, and disabled account all return the same generic "invalid credentials" error, so the endpoint leaks no account existence or status.
Endpoints¶
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/auth/register |
Redeem an invite key + create an account (logs in) |
GET |
/api/v1/auth/check-username |
Live availability check (UX) |
POST |
/api/v1/auth/login |
Username/password → session cookie |
POST |
/api/v1/auth/logout |
Revoke the session + clear the cookie |
POST |
/api/v1/auth/change-password |
Change own password (verifies current, revokes all other sessions, re-issues the caller's) |
GET |
/api/v1/auth/accounts |
List accounts (admin/owner) |
POST |
/api/v1/auth/accounts/{id}/disable |
Disable + kill sessions |
POST |
/api/v1/auth/accounts/{id}/enable |
Re-enable |
See API authentication for the key model and Environment reference for the session settings.
Security notes¶
- Passwords are hashed with Argon2id (
argon2-cffi); plaintext and hashes are never logged. - Session tokens are random (
secrets.token_urlsafe(32)) and stored only as a SHA-256 hash — a DB leak hands out no live sessions. - Login is rate-limited per IP+username (in-process; see the API auth doc for the scale-out caveat).