Skip to content

Users management

Purpose

The Users screen is the operator's master directory of every player on the platform: search, filter, paginate, and click into a user to act on them. It is the gateway to all per-user actions documented in user-profile.md. Searches and listings are page-cursor-based; the same query options are reused by the user-picker on the affiliates and tips screens.

Audience

Customer support (primary), compliance (KYC reviews), operations (bans / withdrawal blocks). Engineering rarely touches this screen.

Path in admin-fe

Screen URL Page Component
List /users ebit-admin-fe/src/app/(dashboard)/users/page.tsx:8 components/pages/users/index.tsx
Filters (in-page) components/pages/users/UsersFilters/ server-side filters
Table (in-page) components/pages/users/UsersTable/ row click → /users/[id]
Toolbar (in-page) components/pages/users/TableToolbar/ search + create-admin button

Backing API endpoints

Endpoint Purpose Source
POST /admin/user/all Paginated user list with stats apps/api/src/user/admin.user.controller.ts:57
GET /admin/user/:id Drill-in (used by /users/[id]) apps/api/src/user/admin.user.controller.ts:248
GET /admin/user/all-permissions Populates the permission-checkbox UI apps/api/src/user/admin.user.controller.ts:82
GET /admin/user/get-all-roles-with-users Role filter dropdown apps/api/src/user/admin.user.controller.ts:66
GET /admin/user/stats/transactions "Top users by transactions" sub-table apps/api/src/user/admin.user.controller.ts:109

Frontend wiring: ebit-admin-fe/src/queries/users/index.tsuseAllUsersQuery, useGetUsersInfiniteQuery, useGetUserStatisticQuery.

Key actions

Action Required permission / role API call DB tables touched Audit-logged?
List users (paginated, sorted) user.view POST /admin/user/all User, UserRole, UserPermission, UserStats yes
Search by username / email / ID user.view same, with search field same yes
Filter by role (admin, streamer, banned…) user.view same, with where.roles same yes
Filter by KYC level user.view + kyc.view same, with where.kycLevel same yes
Sort by createdAt / balance / wagered USD user.view same, with sortBy=ID|CREATED_AT|... same yes
Open profile user.view GET /admin/user/:id User, joined relations yes
Top users by tx user.transactions.view GET /admin/user/stats/transactions Transaction, User yes

AdminActionLog rows: every list/read writes a row keyed on userId/adminUserId. See data-model/ for AdminActionLog schema (api.prisma:1479-1497).

Filters and views

From UsersFilters/ and the TGetAllUsersOptions type (ebit-admin-fe/src/types):

  • Search — username, email, ID, twitch handle.
  • Role — admin, streamer, affiliate, banned, KYC-pending, etc.
  • KYC level — Sumsub levels (0-3); see kyc-limits.md.
  • Country — joined via the Country table.
  • Date range — registration date (createdAt).
  • Balance range — USD-equivalent.
  • Wagered range — total wager USD.
  • SortEUserSortBy = ID | CREATED_AT | BALANCE | WAGERED_USD, asc/desc.
  • withBalance / withStatsUsd toggles — drop these to make the query 5× faster on broad filters; admin-fe sets them true by default.

Common workflows

  1. Customer support — find a user by email. CS receives a Zendesk ticket. They paste the email into the search box. The endpoint matches case-insensitive with ILIKE. If multiple matches, sort by createdAt DESC. Click into the right user → see user-profile.md.
  2. Compliance — review high-balance KYC-pending users. Filter kycLevel=0 AND sort by BALANCE DESC. Cross-reference top 50 with kyc-limits.md. Open each profile and call GET /admin/kyc/:userId/info.
  3. Operations — find players from a restricted geography. Filter by country=RU. Pull the list, ban or block withdrawals one by one — see user-profile.md.
  4. Marketing — find top wagering players for VIP outreach. Sort by WAGERED_USD DESC, take top 100. Forward to vip-program.md review queue.
  5. Forensics — daily new-account scan. Filter createdAt >= today, look for clusters of similar usernames or shared IPs (cross-check with admin-logs.md by IP).

Edge cases / gotchas

  • search is case-insensitive but accent-sensitive. "müller" won't match "muller". This is a Prisma/Postgres limitation; deal with it by trying both spellings.
  • withStatsUsd=true is expensive. It joins UserStats and converts via exchange-rates. On searches that return >5k rows this can blow the 5s frontend timeout. Narrow filters first.
  • role=Banned is a virtual filter. There is no Banned role; the query filters on User.bannedAt IS NOT NULL. Look for it in apps/api/src/user/user.service.ts (findManyUsersWithStats).
  • No bulk actions. Multi-select exists in the table UI (SelectMore/) but bulk ban / bulk message is NOT IMPLEMENTED — see admin.user.controller.ts:147 // TODO implement rout for multi ban.
  • The "Online" indicator is per-user, not aggregate. Profile-level online flag is real (driven by Auth.session), but the count summary at the top of /users reuses the inflated fakeUserOnline series — see project_online_count_inflation memory.
  • Pagination cursor is page-numbered, not cursor-based. Stable while no inserts/deletes happen mid-scan. With heavy churn, you may see a row twice. Live with it; the alternative is unbounded cursor queries.

Sequence — opening the Users page and filtering

sequenceDiagram
    actor admin
    participant admin-fe
    participant api
    participant pg as Postgres
    admin->>admin-fe: GET /users
    admin-fe->>api: GET /admin/user/get-all-roles-with-users
    api-->>admin-fe: roles
    admin-fe->>api: GET /admin/user/all-permissions
    api-->>admin-fe: permissions catalog (60+ keys)
    admin->>admin-fe: pick role=Streamer, sort=BALANCE DESC
    admin-fe->>api: POST /admin/user/all { sortOrder, sortBy, where }
    api->>api: PermissionGuard('user.view')
    api->>pg: SELECT user, JOIN userRole, userStats, balance
    pg-->>api: rows + total count
    api-->>admin-fe: { data: User[], paginationMeta }
    admin-fe-->>admin: rendered table
    admin->>admin-fe: click user row
    admin-fe->>admin-fe: navigate /users/[id]
    Note over admin-fe,api: see user-profile.md for the rest