diff --git a/keyserver/src/creators/invite-link-creator.js b/keyserver/src/creators/invite-link-creator.js new file mode 100644 --- /dev/null +++ b/keyserver/src/creators/invite-link-creator.js @@ -0,0 +1,121 @@ +// @flow + +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 createIDs from './id-creator.js'; +import { dbQuery, 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 { Viewer } from '../session/viewer.js'; + +const secretRegex = /^[a-zA-Z0-9]+$/; + +async function createOrUpdatePublicLink( + viewer: Viewer, + request: CreateOrUpdatePublicLinkRequest, +): Promise { + if (!secretRegex.test(request.name)) { + throw new ServerError('invalid_parameters'); + } + + const permissionPromise = checkThreadPermission( + viewer, + request.communityID, + threadPermissions.MANAGE_INVITE_LINKS, + ); + const existingPrimaryLinksPromise = fetchPrimaryInviteLinks(viewer); + const fetchThreadInfoPromise = fetchServerThreadInfos( + SQL`t.id = ${request.communityID}`, + ); + const [hasPermission, existingPrimaryLinks, { threadInfos }] = + await Promise.all([ + permissionPromise, + existingPrimaryLinksPromise, + fetchThreadInfoPromise, + ]); + if (!hasPermission) { + throw new ServerError('invalid_credentials'); + } + 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 { + 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; + try { + result = (await dbQuery(createLinkQuery))[0]; + } catch { + throw new ServerError('invalid_parameters'); + } + + if (result.affectedRows === 0) { + const deleteIDs = SQL` + DELETE FROM ids + WHERE id = ${id} + `; + 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, + }; +} + +export { createOrUpdatePublicLink }; diff --git a/keyserver/src/endpoints.js b/keyserver/src/endpoints.js --- a/keyserver/src/endpoints.js +++ b/keyserver/src/endpoints.js @@ -20,6 +20,7 @@ import type { JSONResponder } from './responders/handlers.js'; import { getSessionPublicKeysResponder } from './responders/keys-responders.js'; import { + createOrUpdatePublicLinkResponder, fetchPrimaryInviteLinksResponder, inviteLinkVerificationResponder, } from './responders/link-responders.js'; @@ -96,6 +97,10 @@ responder: multimediaMessageCreationResponder, requiredPolicies: baseLegalPolicies, }, + create_or_update_public_link: { + responder: createOrUpdatePublicLinkResponder, + requiredPolicies: baseLegalPolicies, + }, create_reaction_message: { responder: reactionMessageCreationResponder, requiredPolicies: baseLegalPolicies, 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 @@ -6,10 +6,13 @@ type InviteLinkVerificationRequest, type InviteLinkVerificationResponse, type FetchInviteLinksResponse, + type InviteLink, inviteLinkValidator, + type CreateOrUpdatePublicLinkRequest, } from 'lib/types/link-types.js'; import { tShape, tID } from 'lib/utils/validation-utils.js'; +import { createOrUpdatePublicLink } from '../creators/invite-link-creator.js'; import { fetchPrimaryInviteLinks, verifyInviteLink, @@ -71,4 +74,27 @@ ); } -export { inviteLinkVerificationResponder, fetchPrimaryInviteLinksResponder }; +const createOrUpdatePublicLinkInputValidator: TInterface = + tShape({ + name: t.String, + communityID: tID, + }); + +async function createOrUpdatePublicLinkResponder( + viewer: Viewer, + input: mixed, +): Promise { + const request = await validateInput( + viewer, + createOrUpdatePublicLinkInputValidator, + input, + ); + const response = await createOrUpdatePublicLink(viewer, request); + return validateOutput(viewer.platformDetails, inviteLinkValidator, response); +} + +export { + inviteLinkVerificationResponder, + fetchPrimaryInviteLinksResponder, + createOrUpdatePublicLinkResponder, +}; diff --git a/lib/types/endpoints.js b/lib/types/endpoints.js --- a/lib/types/endpoints.js +++ b/lib/types/endpoints.js @@ -51,6 +51,7 @@ CREATE_ERROR_REPORT: 'create_error_report', CREATE_MESSAGE_REPORT: 'create_message_report', CREATE_MULTIMEDIA_MESSAGE: 'create_multimedia_message', + CREATE_OR_UPDATE_PUBLIC_LINK: 'create_or_update_public_link', CREATE_REACTION_MESSAGE: 'create_reaction_message', EDIT_MESSAGE: 'edit_message', CREATE_TEXT_MESSAGE: 'create_text_message', 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 @@ -55,3 +55,8 @@ export type InviteLinksStore = { +links: InviteLinks, }; + +export type CreateOrUpdatePublicLinkRequest = { + +name: string, + +communityID: string, +};