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
4 changes: 4 additions & 0 deletions .changeset/david-event-mgmt-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Fix chapter leader event management permission checks for users added via Slack
4 changes: 3 additions & 1 deletion server/src/addie/member-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions server/src/db/outbound-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
),
Expand Down
45 changes: 28 additions & 17 deletions server/src/db/working-group-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<WorkingGroupLeader>(
`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)), ''),
Expand All @@ -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
Expand All @@ -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<WorkingGroupLeader & { working_group_id: string }>(
`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)), ''),
Expand All @@ -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
Expand Down Expand Up @@ -749,34 +754,40 @@ 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<boolean> {
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;
}

/**
* 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<WorkingGroupWithMemberCount[]> {
const canonicalUserId = await this.resolveToCanonicalUserId(userId);
const result = await query<WorkingGroupWithMemberCount>(
`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;
Expand Down
11 changes: 9 additions & 2 deletions server/src/routes/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down