diff --git a/.changeset/david-event-mgmt-fix.md b/.changeset/david-event-mgmt-fix.md new file mode 100644 index 0000000000..b0344a2e57 --- /dev/null +++ b/.changeset/david-event-mgmt-fix.md @@ -0,0 +1,4 @@ +--- +--- + +Fix chapter leader event management permission checks for users added via Slack diff --git a/server/src/addie/member-context.ts b/server/src/addie/member-context.ts index 49cd49a006..b07df117e0 100644 --- a/server/src/addie/member-context.ts +++ b/server/src/addie/member-context.ts @@ -34,11 +34,13 @@ async function getPendingContentForUser( const pool = getPool(); // Get committees user leads + // Join with slack_user_mappings to handle users who were added as leader via Slack ID const leaderResult = await pool.query( `SELECT wg.id, wg.name, wg.slug FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL JOIN working_groups wg ON wg.id = wgl.working_group_id - WHERE wgl.user_id = $1`, + WHERE wgl.user_id = $1 OR sm.workos_user_id = $1`, [workosUserId] ); const ledCommitteeIds = leaderResult.rows.map(c => c.id); diff --git a/server/src/db/outbound-db.ts b/server/src/db/outbound-db.ts index 695c0fec36..9997fba7d8 100644 --- a/server/src/db/outbound-db.ts +++ b/server/src/db/outbound-db.ts @@ -745,13 +745,17 @@ export async function getMemberCapabilities( WHERE wg.committee_type = 'working_group' AND ( EXISTS(SELECT 1 FROM working_group_memberships wgm WHERE wgm.working_group_id = wg.id AND wgm.workos_user_id = $1) - OR EXISTS(SELECT 1 FROM working_group_leaders wgl WHERE wgl.working_group_id = wg.id AND wgl.user_id = $1) + OR EXISTS(SELECT 1 FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + WHERE wgl.working_group_id = wg.id AND (wgl.user_id = $1 OR sm.workos_user_id = $1)) )) as wg_count, (SELECT COUNT(DISTINCT wg.id) FROM working_groups wg WHERE wg.committee_type = 'council' AND ( EXISTS(SELECT 1 FROM working_group_memberships wgm WHERE wgm.working_group_id = wg.id AND wgm.workos_user_id = $1) - OR EXISTS(SELECT 1 FROM working_group_leaders wgl WHERE wgl.working_group_id = wg.id AND wgl.user_id = $1) + OR EXISTS(SELECT 1 FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + WHERE wgl.working_group_id = wg.id AND (wgl.user_id = $1 OR sm.workos_user_id = $1)) )) as council_count`, [workosUserId] ), diff --git a/server/src/db/working-group-db.ts b/server/src/db/working-group-db.ts index be51e90fc1..c777a3fc67 100644 --- a/server/src/db/working-group-db.ts +++ b/server/src/db/working-group-db.ts @@ -592,11 +592,12 @@ export class WorkingGroupDatabase { // 3. organization_memberships (older sync table) // 4. Falls back to user_id if no name found // - // Note: canonical_user_id equals user_id since we store canonical IDs at write time + // canonical_user_id is resolved at read time via slack_user_mappings + // to handle cases where leaders were added via Slack ID before linking WorkOS const result = await query( `SELECT wgl.user_id, - wgl.user_id AS canonical_user_id, + COALESCE(sm.workos_user_id, wgl.user_id) AS canonical_user_id, COALESCE( NULLIF(wgm.user_name, ''), NULLIF(TRIM(CONCAT(u.first_name, ' ', u.last_name)), ''), @@ -608,13 +609,14 @@ export class WorkingGroupDatabase { COALESCE(wgm.user_org_name, user_org.name, org.name) AS org_name, wgl.created_at FROM working_group_leaders wgl - LEFT JOIN working_group_memberships wgm ON wgl.user_id = wgm.workos_user_id AND wgm.working_group_id = wgl.working_group_id - LEFT JOIN users u ON wgl.user_id = u.workos_user_id + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + LEFT JOIN working_group_memberships wgm ON COALESCE(sm.workos_user_id, wgl.user_id) = wgm.workos_user_id AND wgm.working_group_id = wgl.working_group_id + LEFT JOIN users u ON COALESCE(sm.workos_user_id, wgl.user_id) = u.workos_user_id LEFT JOIN organizations user_org ON u.primary_organization_id = user_org.workos_organization_id LEFT JOIN LATERAL ( SELECT om.first_name, om.last_name, om.email, om.workos_organization_id FROM organization_memberships om - WHERE om.workos_user_id = wgl.user_id + WHERE om.workos_user_id = COALESCE(sm.workos_user_id, wgl.user_id) ORDER BY om.created_at DESC LIMIT 1 ) om ON true @@ -635,11 +637,13 @@ export class WorkingGroupDatabase { return new Map(); } + // canonical_user_id is resolved at read time via slack_user_mappings + // to handle cases where leaders were added via Slack ID before linking WorkOS const result = await query( `SELECT wgl.working_group_id, wgl.user_id, - wgl.user_id AS canonical_user_id, + COALESCE(sm.workos_user_id, wgl.user_id) AS canonical_user_id, COALESCE( NULLIF(wgm.user_name, ''), NULLIF(TRIM(CONCAT(u.first_name, ' ', u.last_name)), ''), @@ -651,13 +655,14 @@ export class WorkingGroupDatabase { COALESCE(wgm.user_org_name, user_org.name, org.name) AS org_name, wgl.created_at FROM working_group_leaders wgl - LEFT JOIN working_group_memberships wgm ON wgl.user_id = wgm.workos_user_id AND wgm.working_group_id = wgl.working_group_id - LEFT JOIN users u ON wgl.user_id = u.workos_user_id + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + LEFT JOIN working_group_memberships wgm ON COALESCE(sm.workos_user_id, wgl.user_id) = wgm.workos_user_id AND wgm.working_group_id = wgl.working_group_id + LEFT JOIN users u ON COALESCE(sm.workos_user_id, wgl.user_id) = u.workos_user_id LEFT JOIN organizations user_org ON u.primary_organization_id = user_org.workos_organization_id LEFT JOIN LATERAL ( SELECT om.first_name, om.last_name, om.email, om.workos_organization_id FROM organization_memberships om - WHERE om.workos_user_id = wgl.user_id + WHERE om.workos_user_id = COALESCE(sm.workos_user_id, wgl.user_id) ORDER BY om.created_at DESC LIMIT 1 ) om ON true @@ -749,14 +754,19 @@ export class WorkingGroupDatabase { /** * Check if a user is a leader of a working group + * Handles both WorkOS and Slack user IDs by checking both directions of the mapping */ async isLeader(workingGroupId: string, userId: string): Promise { - const canonicalUserId = await this.resolveToCanonicalUserId(userId); + // Check if: + // 1. The leader record has the user ID directly, OR + // 2. The leader record has a Slack ID that maps to this WorkOS user ID const result = await query( - `SELECT 1 FROM working_group_leaders - WHERE working_group_id = $1 AND user_id = $2 + `SELECT 1 FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + WHERE wgl.working_group_id = $1 + AND (wgl.user_id = $2 OR sm.workos_user_id = $2) LIMIT 1`, - [workingGroupId, canonicalUserId] + [workingGroupId, userId] ); return result.rows.length > 0; } @@ -764,19 +774,20 @@ export class WorkingGroupDatabase { /** * Get all committees led by a user * Returns committees of all types where the user is a leader + * Handles both WorkOS and Slack user IDs by checking both directions of the mapping */ async getCommitteesLedByUser(userId: string): Promise { - const canonicalUserId = await this.resolveToCanonicalUserId(userId); const result = await query( - `SELECT wg.*, COUNT(wgm.id)::int AS member_count + `SELECT wg.*, COUNT(DISTINCT wgm.id)::int AS member_count FROM working_groups wg INNER JOIN working_group_leaders wgl ON wg.id = wgl.working_group_id + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL LEFT JOIN working_group_memberships wgm ON wg.id = wgm.working_group_id AND wgm.status = 'active' - WHERE wgl.user_id = $1 + WHERE (wgl.user_id = $1 OR sm.workos_user_id = $1) AND wg.status = 'active' GROUP BY wg.id ORDER BY wg.display_order, wg.name`, - [canonicalUserId] + [userId] ); const groups = result.rows; diff --git a/server/src/routes/content.ts b/server/src/routes/content.ts index 79ba41bca4..598bd4c909 100644 --- a/server/src/routes/content.ts +++ b/server/src/routes/content.ts @@ -104,9 +104,14 @@ export function createContentRouter(): Router { ); // Get committees user is a member of (non-public ones) + // Join with slack_user_mappings to handle users who were added as leader via Slack ID const memberResult = await pool.query( `SELECT wg.id, wg.slug, wg.name, wg.description, - EXISTS(SELECT 1 FROM working_group_leaders wgl WHERE wgl.working_group_id = wg.id AND wgl.user_id = $1) as is_leader + EXISTS( + SELECT 1 FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL + WHERE wgl.working_group_id = wg.id AND (wgl.user_id = $1 OR sm.workos_user_id = $1) + ) as is_leader FROM working_group_memberships wgm JOIN working_groups wg ON wg.id = wgm.working_group_id WHERE wgm.workos_user_id = $1 @@ -320,11 +325,13 @@ export function createContentRouter(): Router { const pool = getPool(); // Get committees user leads + // Join with slack_user_mappings to handle users who were added as leader via Slack ID const leaderResult = await pool.query( `SELECT wg.id, wg.name, wg.slug FROM working_group_leaders wgl + LEFT JOIN slack_user_mappings sm ON wgl.user_id = sm.slack_user_id AND sm.workos_user_id IS NOT NULL JOIN working_groups wg ON wg.id = wgl.working_group_id - WHERE wgl.user_id = $1`, + WHERE wgl.user_id = $1 OR sm.workos_user_id = $1`, [user.id] ); const ledCommittees = leaderResult.rows;