Users
The Users tab is the per-app roster of accounts that have registered through the client SDK: one row per end_users document, created on register and refreshed on every login. It lives inside an application (Applications, open one, Users), and two apps never share a user list even when the same person registers with the same username on both.
When the tab is empty
Three situations produce a stubbornly empty table.
- The app runs in license-only mode. Clients call
/api/client/license, which validates the key and sets itsused_byto"license-only"without ever touchingend_users. Manage those activations from the Keys tab instead. - The app uses username and password auth, but no client has called
registeryet. - You are signed in as a reseller without
view_keys. In that case the tab is hidden from the sidebar entirely; if you can see it, you can list it.
Columns
The list endpoint is GET /api/apps/{app_id}/users, defined in backend/routers/licenses.py. Every column below is populated from a single response.
| Column | Source field | Notes |
|---|---|---|
| Username | username | Click to copy. |
| Created | created_at | Server timestamp when register was accepted. Hover for the raw ISO value. |
| Last Login | last_login | Set on every successful /api/client/login. Shows never for a user that has registered but not yet logged in. |
| IP | ip | Last IP observed on login. Click to copy. Hidden from resellers that lack view_ip. |
| HWID | hwid | Current HWID lock when the app has HWID lock enabled. Click to copy. Hidden from resellers that lack view_hwid. |
| Level | level | Subscription level assigned by whichever license activated the user. |
| Location | last_geo | Country and city derived from the last login IP. |
| Screen | last_screenshot_path | Thumbnail of the screenshot captured on the last activation, when screenshot capture is enabled on the app. |
| Expires | expires_at | Date-only slice of the ISO timestamp, or ∞ when the account has no expiry. |
| Status | banned | active (green) or banned (red). |
The column count adapts to your role. IP and HWID drop out entirely for resellers without those permissions, and the empty-state row's colSpan shifts to match.
Screenshot thumbnails require both view_ip and view_hwid. A reseller missing either permission sees an empty Screen cell regardless of whether capture is enabled on the app.
Click to copy
Username, IP, and HWID render through the internal CopyText component. Click a value and it goes to your clipboard via navigator.clipboard.writeText, with a green check that flashes for about a second. On older browsers or insecure origins the component falls back to a hidden <textarea> and document.execCommand("copy").
There is no keyboard shortcut. Point, click, done.
Actions
Every row shows up to three icon buttons on the right, each gated by a reseller permission. Owners, admins, and developers see all three no matter what.
| Icon | Action | Endpoint | Required permission |
|---|---|---|---|
| Counter-clockwise arrow | Reset HWID | POST /api/users/{user_id}/reset-hwid | reset_hwid |
| Prohibit | Ban / Unban | PATCH /api/users/{user_id}/ban with {"banned": true | false} | freeze_unfreeze |
| Trash | Delete user | DELETE /api/users/{user_id} | delete_keys |
Reset HWID wipes both hwid and known_hwids on the user document. The next successful login re-locks the account to whatever device authenticated. Use it for legitimate hardware swaps.
Ban flips banned to true. The row stays, but every future /api/client/login returns 401 Invalid username or password, deliberately indistinguishable from a wrong password. Unban to reverse.
Delete is permanent. The end_users document is removed and any live heartbeat session referencing it becomes orphaned. The underlying license key stays intact and reusable unless you also revoke it from the Keys tab.
All three actions write an audit event that shows up in the Logs tab.
Reseller visibility
Reseller scoping is enforced server-side in backend/access.py, not just hidden in the UI. Two mechanisms combine.
scoped_user_filter narrows the query to reseller_id ∈ {reseller_id, ...admin descendants}. A reseller only ever sees users registered under keys they, or resellers they manage, generated. Owners and admins with no parent see everything for the app.
strip_reseller_hidden then runs on every returned document and pops fields the reseller is not allowed to see:
- No
view_hwid:hwidandknown_hwidsare removed. - No
view_ip:ipandknown_countriesare removed, and geo objects (geo,last_geo,activation_geo) haveip,city,region,country,lat,lonscrubbed from them. - Missing either
view_hwidorview_ip: screenshot paths are removed, leaving the Screen column empty.
The dashboard mirrors both rules with CanReseller("view_ip") and CanReseller("view_hwid") so the affected columns never render as blank placeholders. Because the filter runs inside the database query itself, a hand-crafted curl from a scoped reseller cannot see a peer's users.
Bulk behavior and limits
The tab has no bulk-select UI. Actions run one row at a time from here; for scripted cleanup, drive the endpoints directly.
The list endpoint returns at most 2000 users per app, sorted by created_at descending. Apps above that cap need paginated calls straight to the API, since the dashboard truncates.
Refresh
The tab does not poll. It fetches once when you open the app and re-fetches after every mutating action (reset HWID, ban, delete). Switch tabs and back, or hit the browser refresh, to force a manual reload.