Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/tame-lions-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
23 changes: 13 additions & 10 deletions server/src/routes/admin/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,20 +254,23 @@ export function createAdminUsersRouter(): Router {
slackOnlyContactsResult.rows.map(r => [r.slack_user_id, r])
);

// Add Slack-only users (those not linked to any AAO account)
// Add Slack users not already processed (Slack-only OR mapped individuals without org)
for (const slackUser of slackMappings) {
if (processedSlackIds.has(slackUser.slack_user_id)) continue;
if (slackUser.workos_user_id) continue;

// Check if this Slack user's email matches an AAO user
if (slackUser.slack_email) {
// Determine if this is a mapped individual (has workos_user_id but no org membership)
// or a true Slack-only user (no workos_user_id)
const isMappedIndividual = !!slackUser.workos_user_id;

// Check if this Slack user's email matches an AAO user (skip to avoid duplicates)
if (slackUser.slack_email && !isMappedIndividual) {
const hasAaoMatch = aaoUsersResult.rows.some(
u => u.email.toLowerCase() === slackUser.slack_email!.toLowerCase()
);
if (hasAaoMatch) continue;
}

// Skip if filtering by group (Slack-only users have no groups)
// Skip if filtering by group (these users have no groups)
if (filterByGroup) continue;

// Get engagement data for this Slack user
Expand All @@ -288,17 +291,17 @@ export function createAdminUsersRouter(): Router {
}

unifiedUsers.push({
workos_user_id: null,
email: null,
name: null,
workos_user_id: isMappedIndividual ? slackUser.workos_user_id : null,
email: slackUser.slack_email,
name: slackUser.slack_real_name || slackUser.slack_display_name || null,
org_id: null,
org_name: null,
slack_user_id: slackUser.slack_user_id,
slack_email: slackUser.slack_email,
slack_display_name: slackUser.slack_display_name,
slack_real_name: slackUser.slack_real_name,
mapping_status: 'slack_only',
mapping_source: null,
mapping_status: isMappedIndividual ? 'mapped' : 'slack_only',
mapping_source: isMappedIndividual ? slackUser.mapping_source : null,
working_groups: [],
// Engagement data from unified contacts
engagement_score: engagement?.engagement_score ?? null,
Expand Down
54 changes: 54 additions & 0 deletions server/src/slack/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface SlackTeamJoinEvent {
user: SlackUser;
}

export interface SlackUserChangeEvent {
type: 'user_change';
user: SlackUser;
}

export interface SlackMemberJoinedChannelEvent {
type: 'member_joined_channel';
user: string; // user ID
Expand Down Expand Up @@ -98,6 +103,7 @@ export interface SlackAppMentionEvent {

export type SlackEvent =
| SlackTeamJoinEvent
| SlackUserChangeEvent
| SlackMemberJoinedChannelEvent
| SlackMessageEvent
| SlackReactionAddedEvent
Expand Down Expand Up @@ -145,6 +151,7 @@ export async function handleTeamJoin(event: SlackTeamJoinEvent): Promise<void> {
slack_real_name: realName,
slack_is_bot: user.is_bot || false,
slack_is_deleted: user.deleted || false,
slack_tz_offset: user.tz_offset ?? null,
});

// Auto-map by email if they have a web account
Expand All @@ -158,6 +165,49 @@ export async function handleTeamJoin(event: SlackTeamJoinEvent): Promise<void> {
}
}

/**
* Handle user_change event - user profile was updated
* Updates our database with the new profile data
*/
export async function handleUserChange(event: SlackUserChangeEvent): Promise<void> {
const user = event.user;

if (!user?.id) {
logger.warn('user_change event missing user data');
return;
}

logger.debug(
{ userId: user.id, email: user.profile?.email, name: user.profile?.real_name },
'Slack user profile changed'
);

try {
const email = user.profile?.email || null;
const displayName = user.profile?.display_name || user.profile?.display_name_normalized || null;
const realName = user.profile?.real_name || user.real_name || null;

// Upsert the user into our database with updated profile
await slackDb.upsertSlackUser({
slack_user_id: user.id,
slack_email: email,
slack_display_name: displayName,
slack_real_name: realName,
slack_is_bot: user.is_bot || false,
slack_is_deleted: user.deleted || false,
slack_tz_offset: user.tz_offset ?? null,
});

// Invalidate caches since user data changed
invalidateUnifiedUsersCache();
invalidateMemberContextCache(user.id);

logger.debug({ userId: user.id }, 'Slack user profile updated');
} catch (error) {
logger.error({ error, userId: user.id }, 'Failed to process user_change event');
}
}

/**
* Try to auto-map a Slack user to a web user by email
* Maps them if the email matches and neither account is already mapped
Expand Down Expand Up @@ -544,6 +594,10 @@ export async function handleSlackEvent(payload: SlackEventPayload): Promise<void
await handleTeamJoin(event as SlackTeamJoinEvent);
break;

case 'user_change':
await handleUserChange(event as SlackUserChangeEvent);
break;

case 'member_joined_channel':
await handleMemberJoinedChannel(event as SlackMemberJoinedChannelEvent);
break;
Expand Down