User Listing API โ Developer Guide
Overview
The User Listing API provides platform administrators with full visibility into all registered Visitors, Malet Owners, and Organizations on the Mallnline platform. Exposed through the nodes subgraph (apps/nodes), these admin-only queries bypass individual privacy settings and aggregate cross-service activity statistics into a single response.
| Component | Purpose | Backend |
|---|---|---|
| AdminUsersResolver | Cursor-paginated listing, count, and detail queries | @Resolver(() => AdminUserNode) |
| AdminUsersService | Paginated Mongo queries, text search, and activity aggregation | Follow + Collection model counts |
| UserActivityStats | Cross-service activity snapshot per user | Local counts + federation stubs |
All operations are gated behind dual-layer access control: the @RequirePermission(Permission.MANAGE_USERS) decorator guard and a runtime PLATFORM_ADMIN role check.
Architecture
graph TD
A["AdminUsersResolver"] --> B["AdminUsersService"]
B --> C["User Model (MongoDB)"]
B --> D["Follow Model (MongoDB)"]
B --> E["Collection Model (MongoDB)"]
A -- "admin header" --> F["@RequirePermission(MANAGE_USERS)"]
A -- "runtime" --> G["assertPlatformAdmin(actor)"]
subgraph "Activity Stats (Local)"
D -- "countDocuments" --> H["followersCount"]
D -- "countDocuments" --> I["followingCount"]
E -- "countDocuments" --> J["collectionsCount"]
end
subgraph "Activity Stats (Federation Stubs)"
K["ordersCount = 0"]
L["bookingsCount = 0"]
M["blogPostsCount = 0"]
end
style F fill:#dc2626,color:#fff
style G fill:#dc2626,color:#fff
style K fill:#6b7280,color:#fff
style L fill:#6b7280,color:#fff
style M fill:#6b7280,color:#fff
How It Fits Together
The nodes subgraph already owns the User entity โ profile data, preferences, privacy settings, and the follow/collection graphs. The admin API adds a privileged view of this data:
- Privacy bypass: Normal user queries respect
profileVisibility: PRIVATEandcontentVisibility: ANONYMOUS. Admin queries return real profile data regardless of these settings. - Activity aggregation: Each
AdminUserNodeincludes aUserActivityStatsobject with counts pulled from the localFollowandCollectionmodels. Cross-service counts (Murchases, bookings, blog posts) default to0and are designed for future federation enrichment. - Text search: The
searchfilter performs case-insensitive partial matching acrossuserId,displayName, andemailfields simultaneously.
GraphQL API
List All Users (Paginated)
The primary query returns a cursor-paginated connection with optional search and sort:
query AdminUsers($first: Int, $after: String, $filter: AdminUserFilter) {
adminUsers(first: $first, after: $after, filter: $filter) {
edges {
node {
id
userId
displayName
email
bio
location
avatarUrl
website
company
createdAt
updatedAt
isPrivate
contentVisibility
activityStats {
followersCount
followingCount
collectionsCount
ordersCount
bookingsCount
blogPostsCount
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
Variables:
{
"first": 20,
"after": null,
"filter": {
"search": "alice",
"sortBy": "DISPLAY_NAME",
"sortOrder": "ASC"
}
}
Total Platform User Count
A lightweight query for dashboard widgets:
query {
adminUserCount
}
Returns a single Int โ the total number of registered users on the platform.
Single User Detail
Fetch a single user's full admin view by their federated ID:
query AdminUser($id: ID!) {
adminUser(id: $id) {
id
userId
displayName
email
bio
location
avatarUrl
website
company
createdAt
updatedAt
isPrivate
contentVisibility
activityStats {
followersCount
followingCount
collectionsCount
ordersCount
bookingsCount
blogPostsCount
}
}
}
Returns a NotFoundError if the user does not exist.
Type Reference
AdminUserNode
The admin-specific view of a Visitor or Malet Owner profile:
| Field | Type | Description |
|---|---|---|
id |
ID! |
Federated user identifier |
userId |
String! |
Login identifier |
displayName |
String |
Display name |
email |
String |
Email address |
bio |
String |
Biography |
location |
String |
Geographic location |
avatarUrl |
String |
Avatar image URL |
website |
String |
Personal website |
company |
String |
Company affiliation |
createdAt |
DateTime! |
Profile creation timestamp |
updatedAt |
DateTime! |
Last profile update |
isPrivate |
Boolean! |
Whether profileVisibility is set to PRIVATE |
contentVisibility |
String! |
Content visibility level (FULL_NAME, FIRST_NAME_ONLY, ANONYMOUS) |
activityStats |
UserActivityStats! |
Aggregated activity statistics |
UserActivityStats
Cross-service activity statistics aggregated per user:
| Field | Type | Source | Description |
|---|---|---|---|
followersCount |
Int! |
Local (Follow model) | Users following this person |
followingCount |
Int! |
Local (Follow model) | Entities this user follows (Malets, Users, Organizations) |
collectionsCount |
Int! |
Local (Collection model) | Wishlists and collections owned |
ordersCount |
Int! |
Federation stub | Murchases placed (0 until murchases subgraph contributes) |
bookingsCount |
Int! |
Federation stub | Service bookings (0 until services subgraph contributes) |
blogPostsCount |
Int! |
Federation stub | Blog posts authored (0 until blogs subgraph contributes) |
Note: Cross-service stats are currently set to
0by the nodes subgraph. When owning subgraphs (murchases, services, blogs) add@providesfields to theAdminUserNodetype, the Gateway will automatically compose the real values.
AdminUserFilter
| Field | Type | Default | Description |
|---|---|---|---|
search |
String |
โ | Case-insensitive partial match across userId, displayName, email |
sortBy |
AdminUserSortField |
CREATED_AT |
Field to sort by |
sortOrder |
AdminSortDirection |
DESC |
Sort direction |
Enums
enum AdminUserSortField {
CREATED_AT // Sort by account creation date (default)
DISPLAY_NAME // Alphabetical by display name
EMAIL // Alphabetical by email
}
enum AdminSortDirection {
ASC // Ascending (oldest first / AโZ)
DESC // Descending (newest first / ZโA)
}
Access Control
Dual-Layer Security
The User Listing API uses defense-in-depth access control:
Declarative guard โ
@RequirePermission(Permission.MANAGE_USERS)on each resolver method. This is processed by the NestJS guard pipeline before the method executes.Runtime check โ
assertPlatformAdmin(actor)verifies the actor'srole === 'PLATFORM_ADMIN'at the start of each method body. This catches edge cases where the permission system may not have the latest role data.
@Query(() => AdminUserConnection)
@RequirePermission(Permission.MANAGE_USERS)
@HandleErrors()
async adminUsers(
@Args() paging: AdminUserPagingArgs,
@Args('filter', { nullable: true }) filter: AdminUserFilter,
@CurrentActor() actor: Actor,
): Promise<AdminUserConnection> {
this.assertPlatformAdmin(actor); // Runtime defense-in-depth
// ...
}
Authentication Header
The Gateway forwards the user header from the Auth service to downstream subgraphs. For admin access, the header must contain a PLATFORM_ADMIN role:
{
"id": "admin-user-id",
"login": "admin",
"role": "PLATFORM_ADMIN"
}
Requests without authentication or with a non-admin role receive a ForbiddenException.
Privacy Bypass
Normal User queries in the nodes subgraph respect privacy settings:
| Setting | Normal Query | Admin Query |
|---|---|---|
profileVisibility: PRIVATE |
โ Hidden from lists | โ Visible |
contentVisibility: ANONYMOUS |
Shows "Anonymous" | โ Shows real name |
contentVisibility: FIRST_NAME_ONLY |
Shows first name only | โ Shows full name |
The AdminUsersService.listUsers() method queries the User model directly without applying privacy filters. The isPrivate and contentVisibility fields are returned so admins can see each user's privacy settings without those settings affecting the query results.
Module Structure
apps/nodes/src/actors/user/
โโโ admin-users.types.ts # GraphQL types, enums, connection types
โโโ admin-users.service.ts # Paginated listing + activity stats
โโโ admin-users.service.spec.ts # 14 unit tests
โโโ admin-users.resolver.ts # Admin-only resolver (3 queries)
โโโ admin-users.resolver.spec.ts # 10 unit tests
โโโ user.module.ts # Registers service + resolver + Follow/Collection models
apps/nodes/test/
โโโ admin-users.e2e-spec.ts # 10 E2E tests
Cross-Service Integration
Dependency Map
| Admin Users depends on | For |
|---|---|
User model (Typegoose) |
Profile data, preferences, privacy settings |
Follow model (Typegoose) |
followersCount and followingCount aggregation |
Collection model (Typegoose) |
collectionsCount aggregation |
@app/common |
Permission.MANAGE_USERS, RequirePermission, CurrentActor, HandleErrors |
Federation Enrichment (Future)
When owning subgraphs contribute @provides annotations to the AdminUserNode type, the cross-service stats will be automatically resolved by the federation gateway:
| Subgraph | Field | Mechanism |
|---|---|---|
murchases |
ordersCount |
Count of Murchases by buyerId |
services |
bookingsCount |
Count of bookings by customerId |
blogs |
blogPostsCount |
Count of posts by authorId |
Testing
Unit Tests
# Run admin-users unit tests (24 tests across 2 suites)
npm run test -- apps/nodes --testPathPattern="admin-users"
Key coverage areas:
- AdminUsersService: Pagination,
hasNextPagedetection, text search filter, regex escaping, sort (field + direction), empty results, user count, user-by-ID, private profile detection, activity stats aggregation, cross-service stubs - AdminUsersResolver: Connection construction, access control (admin/non-admin/unauthenticated), filter pass-through, cursor forwarding, limit capping (max 100), user count delegation, single user detail, not-found error
E2E Tests
# Run admin-users E2E tests (10 tests)
npx jest --config apps/nodes/test/jest-e2e.json --testPathPattern="admin-users" --detectOpenHandles
Covers:
- Paginated user listing with all fields and activity stats
- PRIVATE profile visibility bypass for admins
- Text search filtering (by display name)
- Unauthenticated request rejection
- Non-admin user rejection (with error message assertion)
- Total user count query
- Single user detail query with full field coverage
- 404 for non-existent user IDs
- Admin access enforcement on all three queries
Related
- Analytics Aggregation API โ Companion admin-only API for revenue and Murchase analytics
- Privacy & Security APIs โ Privacy bypass logic and content visibility settings
- Organizations & Permissions โ
MANAGE_USERSpermission andPLATFORM_ADMINrole check used for access control - Platform Admin Provisioning โ Dedicated
platform_adminstable backing thePLATFORM_ADMINrole