diff --git a/keyserver/src/creators/invite-link-creator.js b/keyserver/src/creators/invite-link-creator.js index b0e0d2663..e633368de 100644 --- a/keyserver/src/creators/invite-link-creator.js +++ b/keyserver/src/creators/invite-link-creator.js @@ -1,159 +1,160 @@ // @flow import Filter from 'bad-words'; +import { inviteLinkBlobHash } from 'lib/shared/invite-links.js'; import type { CreateOrUpdatePublicLinkRequest, InviteLink, } from 'lib/types/link-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { reservedUsernamesSet } from 'lib/utils/reserved-users.js'; import createIDs from './id-creator.js'; import { dbQuery, MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE, SQL, } from '../database/database.js'; import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { download, type BlobDownloadResult } from '../services/blob.js'; import { Viewer } from '../session/viewer.js'; const secretRegex = /^[a-zA-Z0-9]+$/; const badWordsFilter = new Filter(); async function createOrUpdatePublicLink( viewer: Viewer, request: CreateOrUpdatePublicLinkRequest, ): Promise { if (!secretRegex.test(request.name)) { throw new ServerError('invalid_characters'); } if (badWordsFilter.isProfane(request.name)) { throw new ServerError('offensive_words'); } if (reservedUsernamesSet.has(request.name)) { throw new ServerError('link_reserved'); } const permissionPromise = checkThreadPermission( viewer, request.communityID, threadPermissions.MANAGE_INVITE_LINKS, ); const existingPrimaryLinksPromise = fetchPrimaryInviteLinks(viewer); const fetchThreadInfoPromise = fetchServerThreadInfos({ threadID: request.communityID, }); const blobDownloadPromise = getInviteLinkBlob(request); const [ hasPermission, existingPrimaryLinks, { threadInfos }, blobDownloadResult, ] = await Promise.all([ permissionPromise, existingPrimaryLinksPromise, fetchThreadInfoPromise, blobDownloadPromise, ]); if (!hasPermission) { 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 existingPrimaryLink = existingPrimaryLinks.find( link => link.communityID === request.communityID && link.primary, ); if (existingPrimaryLink) { const query = SQL` UPDATE invite_links SET name = ${request.name} WHERE \`primary\` = 1 AND community = ${request.communityID} `; try { await dbQuery(query); } catch (e) { if (e.errno === MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE) { throw new ServerError('already_in_use'); } throw new ServerError('invalid_parameters'); } return { name: request.name, primary: true, role: defaultRoleID, communityID: request.communityID, expirationTime: null, limitOfUses: null, numberOfUses: 0, }; } const [id] = await createIDs('invite_links', 1); const row = [id, request.name, true, request.communityID, defaultRoleID]; const createLinkQuery = SQL` INSERT INTO invite_links(id, name, \`primary\`, community, role) SELECT ${row} WHERE NOT EXISTS ( SELECT i.id FROM invite_links i WHERE i.\`primary\` = 1 AND i.community = ${request.communityID} ) `; let result = null; const deleteIDs = SQL` DELETE FROM ids WHERE id = ${id} `; try { result = (await dbQuery(createLinkQuery))[0]; } catch (e) { await dbQuery(deleteIDs); if (e.errno === MYSQL_DUPLICATE_ENTRY_FOR_KEY_ERROR_CODE) { throw new ServerError('already_in_use'); } throw new ServerError('invalid_parameters'); } if (result.affectedRows === 0) { await dbQuery(deleteIDs); throw new ServerError('invalid_parameters'); } return { name: request.name, primary: true, role: defaultRoleID, communityID: request.communityID, expirationTime: null, limitOfUses: null, numberOfUses: 0, }; } function getInviteLinkBlob( request: CreateOrUpdatePublicLinkRequest, ): Promise { - const hash = `invite_${request.name}`; + const hash = inviteLinkBlobHash(request.name); return download(hash); } export { createOrUpdatePublicLink }; diff --git a/keyserver/src/deleters/link-deleters.js b/keyserver/src/deleters/link-deleters.js index dc5983aaa..c4989f379 100644 --- a/keyserver/src/deleters/link-deleters.js +++ b/keyserver/src/deleters/link-deleters.js @@ -1,43 +1,44 @@ // @flow +import { inviteLinkBlobHash } from 'lib/shared/invite-links.js'; import type { DisableInviteLinkRequest } from 'lib/types/link-types.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { dbQuery, SQL } from '../database/database.js'; import { checkThreadPermission } from '../fetchers/thread-permission-fetchers.js'; import { deleteBlob } from '../services/blob.js'; import { Viewer } from '../session/viewer.js'; async function deleteInviteLink( viewer: Viewer, request: DisableInviteLinkRequest, ): Promise { const hasPermission = await checkThreadPermission( viewer, request.communityID, threadPermissions.MANAGE_INVITE_LINKS, ); if (!hasPermission) { throw new ServerError('invalid_credentials'); } const query = SQL` DELETE FROM invite_links WHERE name = ${request.name} AND community = ${request.communityID} RETURNING blob_holder AS blobHolder `; const [[row]] = await dbQuery(query); if (row?.blobHolder) { await deleteBlob( { - hash: `invite_${request.name}`, + hash: inviteLinkBlobHash(request.name), holder: row.blobHolder, }, true, ); } } export { deleteInviteLink }; diff --git a/lib/shared/invite-links.js b/lib/shared/invite-links.js index 868f238d1..4f9747e53 100644 --- a/lib/shared/invite-links.js +++ b/lib/shared/invite-links.js @@ -1,14 +1,18 @@ // @flow const inviteLinkErrorMessages: { +[string]: string } = { invalid_characters: 'Link cannot contain any spaces or special characters.', offensive_words: 'No offensive or abusive words allowed.', already_in_use: 'Public link URL already in use.', link_reserved: 'This public link is currently reserved. Please contact support@' + 'comm.app if you would like to claim this link.', }; const defaultErrorMessage = 'Unknown error.'; -export { inviteLinkErrorMessages, defaultErrorMessage }; +function inviteLinkBlobHash(secret: string): string { + return `invite_${secret}`; +} + +export { inviteLinkErrorMessages, defaultErrorMessage, inviteLinkBlobHash };