const codeengine = require('codeengine'); /** * Duplicates a Domo user — copies role, profile details, and group memberships * to a new user with the given name and email. * * @param {text} sourceUserId - The Domo user ID to duplicate * @param {text} newDisplayName - Full name for the new user * @param {text} newEmail - Email address for the new user * @returns {object} - Summary of the created user and copied attributes */ async function duplicateUser(sourceUserId, newDisplayName, newEmail) { // --- 1. Fetch source user --- const response = await codeengine.sendRequest( 'GET', `/api/identity/v1/users/${sourceUserId}?parts=DETAILED` ); // Response wraps user in a { users: [...] } envelope const sourceUser = response?.users?.[0] ?? response; if (!sourceUser || !sourceUser.id) { throw new Error(`Source user ${sourceUserId} not found. Response: ${JSON.stringify(response)}`); } console.log(`Source user: ${sourceUser.displayName} (${sourceUser.id})`); // Extract role ID const roleId = sourceUser.roleId || sourceUser.role || 2; // default to Participant if missing // --- 2. Create new user --- const createResponse = await codeengine.sendRequest( 'POST', '/api/content/v3/users?sendInvite=true', { displayName: newDisplayName, roleId: roleId, detail: { email: newEmail } } ); const newUserId = createResponse.id || createResponse.userId; if (!newUserId) { throw new Error(`Failed to create user. Response: ${JSON.stringify(createResponse)}`); } console.log(`Created user: ${newDisplayName} (${newUserId})`); // --- 3. Copy profile fields via bulk update --- const profileFields = {}; const fieldsToCopy = [ 'title', 'department', 'phoneNumber', 'employeeLocation', 'timeZone', 'employeeNumber', 'employeeId', 'hireDate', 'reportsTo' ]; for (const field of fieldsToCopy) { const value = sourceUser[field]; if (value !== undefined && value !== null && value !== '') { profileFields[field] = value; } } const copiedFields = Object.keys(profileFields); if (copiedFields.length > 0) { const transactionId = generateUUID(); await codeengine.sendRequest( 'PUT', '/api/content/v2/users/bulk', { transactionId: transactionId, users: [ { id: String(newUserId), displayName: newDisplayName, ...profileFields } ] } ); console.log(`Copied profile fields: ${copiedFields.join(', ')}`); } // --- 4. Copy locale if present --- if (sourceUser.locale) { await codeengine.sendRequest( 'PATCH', `/api/identity/v1/users/${newUserId}`, { attributes: [ { key: 'locale', values: [sourceUser.locale] } ] } ); copiedFields.push('locale'); console.log(`Copied locale: ${sourceUser.locale}`); } // --- 5. Find source user's group memberships --- const userGroups = []; let offset = 0; const limit = 100; let hasMore = true; while (hasMore) { const groupsPage = await codeengine.sendRequest( 'GET', `/api/content/v2/groups/grouplist?includeFullMembership=true&limit=${limit}&offset=${offset}` ); const groups = Array.isArray(groupsPage) ? groupsPage : (groupsPage.groups || []); for (const group of groups) { const members = group.members || group.users || group.userIds || []; const isMember = members.some(m => { if (typeof m === 'object') { return String(m.id) === String(sourceUserId) || String(m.userId) === String(sourceUserId); } return String(m) === String(sourceUserId); }); if (isMember) { userGroups.push({ groupId: group.id, groupName: group.name }); } } hasMore = groups.length >= limit; offset += limit; } console.log(`Found ${userGroups.length} group memberships`); // --- 6. Add new user to groups --- if (userGroups.length > 0) { const accessPayload = userGroups.map(g => ({ groupId: g.groupId, addMembers: [ { type: 'USER', id: String(newUserId) } ] })); await codeengine.sendRequest('PUT', '/api/content/v2/groups/access', accessPayload); console.log(`Added new user to ${userGroups.length} groups`); } // --- 7. Copy card access --- const cardIds = []; let cardOffset = 0; const cardLimit = 100; let moreCards = true; while (moreCards) { const cardsPage = await codeengine.sendRequest( 'GET', `/api/content/v1/access/users/${sourceUserId}/cards?limit=${cardLimit}&offset=${cardOffset}` ); const cards = Array.isArray(cardsPage) ? cardsPage : (cardsPage.cards || cardsPage.cardIds || []); for (const card of cards) { const cardId = typeof card === 'object' ? (card.id || card.cardId) : card; if (cardId) cardIds.push(String(cardId)); } moreCards = cards.length >= cardLimit; cardOffset += cardLimit; } console.log(`Found ${cardIds.length} cards to share`); // Share cards in batches of 100 (avoid oversized payloads) for (let i = 0; i < cardIds.length; i += 100) { const batch = cardIds.slice(i, i + 100); await codeengine.sendRequest('POST', '/api/content/v1/share?sendEmail=false', { resources: batch.map(id => ({ type: 'badge', id })), recipients: [{ type: 'user', id: String(newUserId) }], message: '' }); } if (cardIds.length > 0) { console.log(`Shared ${cardIds.length} cards with new user`); } // --- 8. Copy page/dashboard access --- const pageIds = []; let pageOffset = 0; const pageLimit = 100; let morePages = true; while (morePages) { const pagesResponse = await codeengine.sendRequest( 'GET', `/api/content/v1/access/users/${sourceUserId}/pages?limit=${pageLimit}&offset=${pageOffset}` ); const pages = Array.isArray(pagesResponse) ? pagesResponse : (pagesResponse.pages || []); for (const page of pages) { const pageId = typeof page === 'object' ? (page.pageId || page.id) : page; if (pageId) pageIds.push(String(pageId)); } morePages = pages.length >= pageLimit; pageOffset += pageLimit; } console.log(`Found ${pageIds.length} pages to share`); // Share pages in batches of 100 for (let i = 0; i < pageIds.length; i += 100) { const batch = pageIds.slice(i, i + 100); await codeengine.sendRequest('POST', '/api/content/v1/share?sendEmail=false', { resources: batch.map(id => ({ type: 'page', id })), recipients: [{ type: 'user', id: String(newUserId) }], message: '' }); } if (pageIds.length > 0) { console.log(`Shared ${pageIds.length} pages with new user`); } // --- 9. Return summary --- return { sourceUser: { id: sourceUser.id, displayName: sourceUser.displayName }, newUser: { id: newUserId, displayName: newDisplayName, email: newEmail }, copiedFields: copiedFields, groupsCopied: userGroups.map(g => g.groupName), cardsShared: cardIds.length, pagesShared: pageIds.length }; } /** * Generate a UUID v4 */ function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } module.exports = duplicateUser;