User Identity Resolution
The Mallnline frontend stores user relationships as opaque UUIDs โ team members, collaborators, blog authors, and Murchase participants are all referenced by their internal userId. Displaying these raw identifiers to end users is a security and UX violation.
The Profile Resolver is a shared utility that batch-resolves user IDs into human-readable profiles (v|handle, displayName, or anonymous fallbacks). It is consumed by every component that displays user attribution, most notably the ActorBadge component.
The Problem
Many backend relationships reference users by their auth-service UUID:
// What the backend returns
{ userId: "a3f8c912-7b4d-4e1a-9f2c-3d5e7a8b1c0d", role: "LEAD" }
// What the user sees (BEFORE fix)
"a3f8c912โฆ"
// What the user sees (AFTER fix)
"@meekdenzo" or "Meek Denzo"
The Handle System provides the sigil taxonomy (v|meekdenzo, @meekdenzo), but until profile data is fetched from the nodes subgraph, the frontend only has raw UUIDs. The Profile Resolver bridges this gap.
Architecture
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ TeamCard.svelteโ โ CollaboratorCard โ โ BlogAnalytics โ
โ TeamsPanel โ โ CollabPanel โ โ โ
โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโฌโโโโโโโโโ
โ โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ profileResolver.ts โ โ Shared utility
โ resolveUserProfilesโ
โ formatUserDisplay โ
โ getAvatarInitials โ
โโโโโโโโโโฌโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ GET_USER_BY_ID โ โ GraphQL query (nodes subgraph)
โ user(id: $id) { โ
โ id, displayName, โ
โ handle, avatarUrlโ
โ } โ
โโโโโโโโโโโโโโโโโโโโโโโ
API Reference
`resolveUserProfiles(userIds, existingCache?)`
Batch-resolves an array of user UUIDs into profile info. Deduplicates before querying and merges results into the provided cache.
import { resolveUserProfiles, type ProfileMap } from '$lib/utils/profileResolver';
const profiles: ProfileMap = await resolveUserProfiles(
['uuid-1', 'uuid-2', 'uuid-3'],
existingCache // Optional โ avoids re-fetching known profiles
);
Parameters:
| Param | Type | Description |
|---|---|---|
userIds |
string[] |
Array of user UUIDs to resolve |
existingCache |
ProfileMap |
Optional existing cache to avoid re-fetching |
Returns: Promise<ProfileMap> โ merged map of userId โ UserProfileInfo
Error handling: Individual resolution failures are silently absorbed โ the profile entry receives a stub { id } so formatUserDisplay falls back to truncated UUID. This prevents a single missing profile from breaking the entire batch.
`formatUserDisplay(profiles, userId)`
Returns the best available display string for a user, following the priority chain:
v|handle โ displayName โ 'Mallnline User'
import { formatUserDisplay } from '$lib/utils/profileResolver';
formatUserDisplay(profiles, 'uuid-1');
// โ "v|meekdenzo" (if handle exists)
// โ "Meek Denzo" (if only displayName exists)
// โ "Mallnline User" (anonymous fallback โ never exposes raw IDs)
`getAvatarInitials(profiles, userId)`
Returns 1โ2 uppercase initials from the resolved displayName, or the first character of the userId as fallback.
import { getAvatarInitials } from '$lib/utils/profileResolver';
getAvatarInitials(profiles, 'uuid-1');
// โ "MD" (from "Meek Denzo")
// โ "M" (from "Madonna")
// โ "MU" (anonymous fallback โ never derives from userId)
Integration Pattern
Components that display user attribution follow a consistent pattern:
1. Parent resolves profiles after data load
// In the parent panel/page (e.g., CollaboratorsPanel.svelte)
import { resolveUserProfiles, type ProfileMap } from '$lib/utils/profileResolver';
let profiles: ProfileMap = $state({});
async function fetchData() {
const data = await client.request(QUERY);
const userIds = data.items.map(item => item.userId);
profiles = await resolveUserProfiles(userIds, profiles);
}
2. Child receives profiles as prop
// In the child card (e.g., CollaboratorCard.svelte)
import { formatUserDisplay, getAvatarInitials, type ProfileMap } from '$lib/utils/profileResolver';
let { memberProfiles = {} }: { memberProfiles?: ProfileMap } = $props();
3. Template renders resolved names
<div class="avatar">{getAvatarInitials(memberProfiles, collaborator.userId)}</div>
<strong>{formatUserDisplay(memberProfiles, collaborator.userId)}</strong>
Components Using Profile Resolution
| Component | Source of User IDs | Resolution Strategy |
|---|---|---|
| TeamCard | TeamMembership.userId |
Profiles passed from org manage page via TeamsPanel |
| CollaboratorCard | Collaborator.userId |
Profiles resolved in CollaboratorsPanel on load |
| BlogAnalytics | TopBlogAuthor.authorId |
Profiles resolved inline after analytics fetch |
| Org Manage Page | org.members[].userId |
Resolves all member profiles on org load (existing resolveMemberProfiles) |
| AuditLogPanel | Audit entry actor IDs | Resolves via GET_USER_BY_ID per unique actor (existing) |
| UserListingPanel | AdminUserNode.userId |
Admin-facing โ intentionally shows raw UUID in detail drawer as debug field |
Relationship to Existing Resolution
The org manage page (/orgs/[slug]/manage) already had a local resolveMemberProfiles() function and getMemberDisplayName() helper. The new profileResolver.ts utility is the extracted, centralized version of this pattern. The manage page's existing implementation remains in place (it predated the utility), but all new components should import from profileResolver.ts.
Migration Path
Existing resolution code in the org manage page can optionally be refactored to use profileResolver.ts:
- import { GET_USER_BY_ID, type UserProfileInfo } from '$lib/queries/auth';
+ import { resolveUserProfiles, formatUserDisplay, type ProfileMap } from '$lib/utils/profileResolver';
- async function resolveMemberProfiles(members) { ... }
- function getMemberDisplayName(userId) { ... }
+ // Use resolveUserProfiles() and formatUserDisplay() directly
This is not urgent โ the existing code works correctly and the utility is API-compatible.
Security Audit Coverage
The Profile Resolver was created as part of the Raw User ID Exposure Audit (P0 Security). The audit covered 10 frontend sub-areas:
| Area | Result |
|---|---|
| uChat user search/DM | โ Already clean |
| Community content (reviews, comments) | โ Already clean |
| Workroom/issue assignment | โ Already clean |
| Team membership lists | โ
Fixed via profileResolver.ts |
| Admin user listing | โ Acceptable (admin field) |
| Murchase/order history | โ Already clean |
| Blog post authorship | โ
Fixed via profileResolver.ts |
| URL parameter leakage | โ Already clean |
| API response sanitization | โ ๏ธ Deferred (backend scope) |
| Error messages | โ Already clean |
File Reference
| File | Purpose |
|---|---|
src/lib/utils/profileResolver.ts |
Shared batch resolver utility |
src/lib/utils/__tests__/profileResolver.test.ts |
10 unit tests |
src/lib/queries/auth.ts |
GET_USER_BY_ID query + UserProfileInfo type |
src/lib/components/teams/TeamCard.svelte |
Team member display (consumes profiles) |
src/lib/components/teams/TeamsPanel.svelte |
Teams container (passes profiles) |
src/lib/components/teams/CollaboratorCard.svelte |
Collaborator display (consumes profiles) |
src/lib/components/teams/CollaboratorsPanel.svelte |
Collaborator container (resolves profiles) |
src/lib/components/admin/BlogAnalytics.svelte |
Blog author leaderboard (resolves profiles) |
Related
- Handle System โ Sigil taxonomy (
v|handle,r|handle) that the resolver surfaces viaformatUserDisplay - ActorBadge Component System โ The UI component that consumes resolved profiles for interactive identity display
- Privacy & Security APIs โ Content visibility settings that control what name is shown publicly
- Teams & Sub-Groups โ TeamCard and TeamsPanel consume resolved profiles
- Outside Collaborators โ CollaboratorCard uses the resolver for external user display
- Platform Admin Provisioning โ Tower Team tab uses a similar pattern for admin profile display
- Support Ticket Anonymity โ Complementary system that hides real user identity behind aliases in support contexts