All files / flows list-members.js

97.87% Statements 46/47
81.57% Branches 31/38
100% Functions 6/6
97.77% Lines 44/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 1821x           1x 1x   1x 1x   1x 20x 20x 20x 20x   20x 20x 20x   20x 1x                   19x 1x                     18x             17x 2x                     15x   15x 1x                     14x             14x 14x                       14x                     13x   13x 6x                     10x 7x       10x           6x     6x 10x 9x                       6x     2x 2x       6x   6x                 3x 3x                    
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb')
const {
  DynamoDBDocumentClient,
  GetCommand,
  QueryCommand,
  BatchGetCommand
} = require('@aws-sdk/lib-dynamodb')
const { permissionChecker } = require('../utils/PermissionChecker')
 
const client = new DynamoDBClient({})
const docClient = DynamoDBDocumentClient.from(client)
 
exports.handler = async (event) => {
  try {
    const flowId = event.pathParameters?.flowId
    const userId = event.requestContext?.authorizer?.claims?.sub
    const searchQueryRaw = event.queryStringParameters?.q
    const searchQuery =
      typeof searchQueryRaw === 'string' ? searchQueryRaw.trim().toLowerCase() : ''
    const limitRaw = Number.parseInt(event.queryStringParameters?.limit ?? '', 10)
    const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined
 
    if (!userId) {
      return {
        statusCode: 401,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Not authenticated' })
      }
    }
 
    if (!flowId) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Missing flowId' })
      }
    }
 
    // Verify user is member of this flow
    const membership = await docClient.send(
      new GetCommand({
        TableName: process.env.FLOW_MEMBERSHIPS_TABLE_NAME,
        Key: { userId, flowId }
      })
    )
 
    if (!membership.Item || membership.Item.status !== 'active') {
      return {
        statusCode: 403,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Access denied - not a member of this flow' })
      }
    }
 
    // Check if user has permission to list members
    const permissionCheck = await permissionChecker.hasPermission(userId, flowId, 'members:list')
 
    if (!permissionCheck.hasPermission) {
      return {
        statusCode: 403,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Forbidden - Insufficient permissions' })
      }
    }
 
    // Get flow info to know the owner
    const flowResult = await docClient.send(
      new GetCommand({
        TableName: process.env.FLOWS_TABLE_NAME,
        Key: { id: flowId }
      })
    )
 
    const flow = flowResult.Item
    Iif (!flow) {
      return {
        statusCode: 404,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify({ error: 'Flow not found' })
      }
    }
 
    // Query all memberships for this flow
    const membershipsResult = await docClient.send(
      new QueryCommand({
        TableName: process.env.FLOW_MEMBERSHIPS_TABLE_NAME,
        IndexName: 'flowIndex',
        KeyConditionExpression: 'flowId = :flowId',
        ExpressionAttributeValues: {
          ':flowId': flowId
        }
      })
    )
 
    const memberships = membershipsResult.Items || []
 
    if (memberships.length === 0) {
      return {
        statusCode: 200,
        headers: {
          'Content-Type': 'application/json',
          'Access-Control-Allow-Origin': '*'
        },
        body: JSON.stringify([])
      }
    }
 
    // Batch get user details
    const userIds = memberships.map((m) => m.userId)
    const usersResult = await docClient.send(
      new BatchGetCommand({
        RequestItems: {
          [process.env.USERS_TABLE_NAME]: {
            Keys: userIds.map((id) => ({ id }))
          }
        }
      })
    )
 
    const users = usersResult.Responses?.[process.env.USERS_TABLE_NAME] || []
 
    // Merge user info with membership info
    const result = memberships.map((membership) => {
      const user = users.find((u) => u.id === membership.userId)
      return {
        userId: membership.userId,
        email: user?.email || 'Unknown',
        name: user?.name,
        roleIds: membership.roleIds,
        status: membership.status,
        invitedBy: membership.invitedBy,
        joinedAt: membership.joinedAt,
        isOwner: membership.userId === flow.ownerId
      }
    })
 
    const filteredResult = searchQuery
      ? result.filter((member) => {
          const haystack =
            `${member.userId || ''} ${member.name || ''} ${member.email || ''}`.toLowerCase()
          return haystack.includes(searchQuery)
        })
      : result
 
    const limitedResult = limit ? filteredResult.slice(0, limit) : filteredResult
 
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(limitedResult)
    }
  } catch (error) {
    console.error('Error:', error)
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({ error: 'Failed to fetch members' })
    }
  }
}