diff --git a/keyserver/src/creators/invite-link-creator.js b/keyserver/src/creators/invite-link-creator.js --- a/keyserver/src/creators/invite-link-creator.js +++ b/keyserver/src/creators/invite-link-creator.js @@ -59,40 +59,61 @@ threadPermissions.MANAGE_INVITE_LINKS, ); const existingPrimaryLinksPromise = fetchPrimaryInviteLinks(viewer); + const threadIDs = new Set([request.communityID]); + if (request.threadID) { + threadIDs.add(request.threadID); + } const fetchThreadInfoPromise = fetchServerThreadInfos({ - threadID: request.communityID, + threadIDs, }); const blobDownloadPromise = getInviteLinkBlob(request.name); + const canManageThreadLinksPromise = request.threadID + ? checkThreadPermission( + viewer, + request.threadID, + threadPermissions.MANAGE_INVITE_LINKS, + ) + : false; const [ hasPermission, existingPrimaryLinks, { threadInfos }, blobDownloadResult, + canManageThreadLinks, ] = await Promise.all([ permissionPromise, existingPrimaryLinksPromise, fetchThreadInfoPromise, blobDownloadPromise, + canManageThreadLinksPromise, ]); - if (!hasPermission) { + if (!hasPermission || (request.threadID && !canManageThreadLinks)) { throw new ServerError('invalid_credentials'); } if (blobDownloadResult.found) { throw new ServerError('already_in_use'); } - const threadInfo = threadInfos[request.communityID]; - if (!threadInfo) { - throw new ServerError('invalid_parameters'); - } - const defaultRoleID = Object.keys(threadInfo.roles).find( - roleID => threadInfo.roles[roleID].isDefault, - ); - if (!defaultRoleID) { - throw new ServerError('invalid_parameters'); + + const defaultRoleIDs: { [string]: string } = {}; + for (const threadID of threadIDs) { + const threadInfo = threadInfos[threadID]; + if (!threadInfo) { + throw new ServerError('invalid_parameters'); + } + const defaultRoleID = Object.keys(threadInfo.roles).find( + roleID => threadInfo.roles[roleID].isDefault, + ); + if (!defaultRoleID) { + throw new ServerError('invalid_parameters'); + } + defaultRoleIDs[threadID] = defaultRoleID; } const existingPrimaryLink = existingPrimaryLinks.find( - link => link.communityID === request.communityID && link.primary, + link => + link.communityID === request.communityID && + link.primary && + (request.threadID ? link.threadID === request.threadID : !link.threadID), ); const blobHolder = uuid.v4(); @@ -109,8 +130,14 @@ const query = SQL` UPDATE invite_links SET name = ${request.name}, blob_holder = ${blobHolder} - WHERE \`primary\` = 1 AND community = ${request.communityID} + WHERE \`primary\` = 1 AND + community = ${request.communityID} `; + if (request.threadID) { + query.append(SQL`AND thread = ${request.threadID}`); + } else { + query.append(SQL`AND thread IS NULL`); + } try { await dbQuery(query); const holder = existingPrimaryLink.blobHolder; @@ -139,7 +166,7 @@ return { name: request.name, primary: true, - role: defaultRoleID, + role: defaultRoleIDs[request.communityID], communityID: request.communityID, expirationTime: null, limitOfUses: null, @@ -154,19 +181,28 @@ request.name, true, request.communityID, - defaultRoleID, + defaultRoleIDs[request.communityID], blobHolder, + request.threadID ?? null, + request.threadID ? defaultRoleIDs[request.threadID] : null, ]; const createLinkQuery = SQL` - INSERT INTO invite_links(id, name, \`primary\`, community, role, blob_holder) + INSERT INTO invite_links(id, name, \`primary\`, community, role, + blob_holder, thread, thread_role) SELECT ${row} WHERE NOT EXISTS ( SELECT i.id FROM invite_links i WHERE i.\`primary\` = 1 AND i.community = ${request.communityID} - ) `; + if (request.threadID) { + createLinkQuery.append(SQL`AND thread = ${request.threadID}`); + } else { + createLinkQuery.append(SQL`AND thread IS NULL`); + } + createLinkQuery.append(SQL`)`); + let result = null; const deleteIDs = SQL` DELETE FROM ids @@ -208,7 +244,7 @@ return { name: request.name, primary: true, - role: defaultRoleID, + role: defaultRoleIDs[request.communityID], communityID: request.communityID, expirationTime: null, limitOfUses: null, diff --git a/keyserver/src/fetchers/link-fetchers.js b/keyserver/src/fetchers/link-fetchers.js --- a/keyserver/src/fetchers/link-fetchers.js +++ b/keyserver/src/fetchers/link-fetchers.js @@ -64,22 +64,33 @@ const query = SQL` SELECT i.name, i.role, i.community, i.expiration_time AS expirationTime, i.limit_of_uses AS limitOfUses, i.number_of_uses AS numberOfUses, - i.\`primary\`, blob_holder AS blobHolder + i.\`primary\`, i.blob_holder AS blobHolder, i.thread, + i.thread_role AS threadRole FROM invite_links i `; query.append(condition); const [result] = await dbQuery(query); - return result.map(row => ({ - name: row.name, - primary: row.primary === 1, - role: row.role.toString(), - communityID: row.community.toString(), - expirationTime: row.expirationTime, - limitOfUses: row.limitOfUses, - numberOfUses: row.numberOfUses, - blobHolder: row.blobHolder, - })); + return result.map(row => { + const link = { + name: row.name, + primary: row.primary === 1, + role: row.role.toString(), + communityID: row.community.toString(), + expirationTime: row.expirationTime, + limitOfUses: row.limitOfUses, + numberOfUses: row.numberOfUses, + blobHolder: row.blobHolder, + }; + if (row.thread && row.threadRole) { + return { + ...link, + threadID: row.thread.toString(), + threadRole: row.threadRole.toString(), + }; + } + return link; + }); } function fetchPrimaryInviteLinks( diff --git a/keyserver/src/responders/link-responders.js b/keyserver/src/responders/link-responders.js --- a/keyserver/src/responders/link-responders.js +++ b/keyserver/src/responders/link-responders.js @@ -48,6 +48,7 @@ tShape({ name: t.String, communityID: tID, + threadID: t.maybe(tID), }); async function createOrUpdatePublicLinkResponder( diff --git a/lib/types/link-types.js b/lib/types/link-types.js --- a/lib/types/link-types.js +++ b/lib/types/link-types.js @@ -28,6 +28,8 @@ +expirationTime: ?number, +limitOfUses: ?number, +numberOfUses: number, + +threadID?: string, + +threadRole?: string, }; export type InviteLinkWithHolder = $ReadOnly<{ @@ -43,6 +45,8 @@ expirationTime: t.maybe(t.Number), limitOfUses: t.maybe(t.Number), numberOfUses: t.Number, + threadID: t.maybe(tID), + threadRole: t.maybe(tID), }); export type FetchInviteLinksResponse = { @@ -73,6 +77,7 @@ export type CreateOrUpdatePublicLinkRequest = { +name: string, +communityID: string, + +threadID?: string, }; export type DisableInviteLinkRequest = {