diff --git a/keyserver/src/fetchers/link-fetchers.js b/keyserver/src/fetchers/link-fetchers.js index 5c478e60f..f3d1b46b8 100644 --- a/keyserver/src/fetchers/link-fetchers.js +++ b/keyserver/src/fetchers/link-fetchers.js @@ -1,125 +1,159 @@ // @flow +import { + FUTURE_CODE_VERSION, + hasMinCodeVersion, +} from 'lib/shared/version-utils.js'; import type { InviteLinkWithHolder, InviteLinkVerificationRequest, InviteLinkVerificationResponse, } from 'lib/types/link-types.js'; import { dbQuery, SQL } from '../database/database.js'; import type { SQLStatementType } from '../database/types.js'; import { Viewer } from '../session/viewer.js'; async function verifyInviteLink( viewer: Viewer, request: InviteLinkVerificationRequest, ): Promise { const query = SQL` - SELECT c.name, i.community AS communityID, m.role + SELECT c.name AS communityName, c.id AS communityID, + cm.role AS communityRole, t.name AS threadName, t.id AS threadID, + tm.role AS threadRole FROM invite_links i INNER JOIN threads c ON c.id = i.community - LEFT JOIN memberships m - ON m.thread = i.community - AND m.user = ${viewer.loggedIn ? viewer.userID : null} + LEFT JOIN memberships cm + ON cm.thread = c.id + AND cm.user = ${viewer.loggedIn ? viewer.userID : null} + LEFT JOIN threads t ON t.id = i.thread + LEFT JOIN memberships tm + ON tm.thread = t.id + AND tm.user = ${viewer.loggedIn ? viewer.userID : null} WHERE i.name = ${request.secret} AND c.community IS NULL + AND (t.community = c.id OR i.thread IS NULL) `; const [result] = await dbQuery(query); if (result.length === 0) { return { status: 'invalid', }; } - const { name, communityID, role } = result[0]; - const status = role > 0 ? 'already_joined' : 'valid'; - return { - status, + const supportsThreadLinks = hasMinCodeVersion(viewer.platformDetails, { + native: FUTURE_CODE_VERSION, + web: FUTURE_CODE_VERSION, + }); + const { + communityName, + communityID, + communityRole, + threadName, + threadID, + threadRole, + } = result[0]; + const communityStatus = communityRole > 0 ? 'already_joined' : 'valid'; + const communityResult = { + status: communityStatus, community: { - name, + name: communityName, id: communityID.toString(), }, }; + if (!threadID || !supportsThreadLinks) { + return communityResult; + } + const threadStatus = threadRole > 0 ? 'already_joined' : 'valid'; + return { + ...communityResult, + status: threadStatus, + thread: { + name: threadName, + id: threadID.toString(), + }, + }; } async function checkIfInviteLinkIsValid( secret: string, communityID: string, ): Promise { const query = SQL` SELECT i.id FROM invite_links i INNER JOIN threads c ON c.id = i.community WHERE i.name = ${secret} AND i.community = ${communityID} AND c.community IS NULL `; const [result] = await dbQuery(query); return result.length === 1; } async function fetchInviteLinksWithCondition( condition: SQLStatementType, ): Promise<$ReadOnlyArray> { 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\`, 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 => { 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( viewer: Viewer, ): Promise<$ReadOnlyArray> { if (!viewer.loggedIn) { return Promise.resolve([]); } const condition = SQL` INNER JOIN memberships m ON i.community = m.thread AND m.user = ${viewer.userID} WHERE i.\`primary\` = 1 AND m.role > 0 `; return fetchInviteLinksWithCondition(condition); } function fetchAllPrimaryInviteLinks(): Promise< $ReadOnlyArray, > { const condition = SQL` WHERE i.\`primary\` = 1 `; return fetchInviteLinksWithCondition(condition); } export { verifyInviteLink, checkIfInviteLinkIsValid, fetchPrimaryInviteLinks, fetchAllPrimaryInviteLinks, }; diff --git a/lib/types/link-types.js b/lib/types/link-types.js index 34912afd1..f206ca9e3 100644 --- a/lib/types/link-types.js +++ b/lib/types/link-types.js @@ -1,91 +1,95 @@ // @flow import t, { type TInterface } from 'tcomb'; import { tID, tShape } from '../utils/validation-utils.js'; export type InviteLinkVerificationRequest = { +secret: string, }; export type InviteLinkVerificationResponse = | { +status: 'valid' | 'already_joined', +community: { +name: string, +id: string, }, + +thread?: { + +name: string, + +id: string, + }, } | { +status: 'invalid' | 'expired', }; export type InviteLink = { +name: string, +primary: boolean, +role: string, +communityID: string, +expirationTime: ?number, +limitOfUses: ?number, +numberOfUses: number, +threadID?: string, +threadRole?: string, }; export type InviteLinkWithHolder = $ReadOnly<{ ...InviteLink, +blobHolder: ?string, }>; export const inviteLinkValidator: TInterface = tShape({ name: t.String, primary: t.Boolean, role: tID, communityID: tID, expirationTime: t.maybe(t.Number), limitOfUses: t.maybe(t.Number), numberOfUses: t.Number, threadID: t.maybe(tID), threadRole: t.maybe(tID), }); export type FetchInviteLinksResponse = { +links: $ReadOnlyArray, }; export type CommunityLinks = { +primaryLink: ?InviteLink, }; export type InviteLinks = { +[communityID: string]: CommunityLinks, }; export type InviteLinksStore = { +links: InviteLinks, }; export const inviteLinksStoreValidator: TInterface = tShape({ links: t.dict( tID, tShape({ primaryLink: t.maybe(inviteLinkValidator), }), ), }); export type CreateOrUpdatePublicLinkRequest = { +name: string, +communityID: string, +threadID?: string, }; export type DisableInviteLinkRequest = { +name: string, +communityID: string, }; export type DisableInviteLinkPayload = { +name: string, +communityID: string, }; diff --git a/lib/types/validators/link-validators.js b/lib/types/validators/link-validators.js index 065df36e6..680698d1f 100644 --- a/lib/types/validators/link-validators.js +++ b/lib/types/validators/link-validators.js @@ -1,29 +1,35 @@ // @flow import t, { type TUnion, type TInterface } from 'tcomb'; import { tShape, tID } from '../../utils/validation-utils.js'; import { type InviteLinkVerificationResponse, type FetchInviteLinksResponse, inviteLinkValidator, } from '../link-types.js'; export const fetchInviteLinksResponseValidator: TInterface = tShape({ links: t.list(inviteLinkValidator), }); +const threadDataValidator = tShape<{ + +name: string, + +id: string, +}>({ + name: t.String, + id: tID, +}); + export const inviteLinkVerificationResponseValidator: TUnion = t.union([ tShape({ status: t.enums.of(['valid', 'already_joined']), - community: tShape({ - name: t.String, - id: tID, - }), + community: threadDataValidator, + thread: t.maybe(threadDataValidator), }), tShape({ status: t.enums.of(['invalid', 'expired']), }), ]);