Developer Docs

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: PRIVATE and contentVisibility: ANONYMOUS. Admin queries return real profile data regardless of these settings.
  • Activity aggregation: Each AdminUserNode includes a UserActivityStats object with counts pulled from the local Follow and Collection models. Cross-service counts (Murchases, bookings, blog posts) default to 0 and are designed for future federation enrichment.
  • Text search: The search filter performs case-insensitive partial matching across userId, displayName, and email fields 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 0 by the nodes subgraph. When owning subgraphs (murchases, services, blogs) add @provides fields to the AdminUserNode type, 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:

  1. Declarative guard โ€” @RequirePermission(Permission.MANAGE_USERS) on each resolver method. This is processed by the NestJS guard pipeline before the method executes.

  2. Runtime check โ€” assertPlatformAdmin(actor) verifies the actor's role === '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, hasNextPage detection, 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