diff --git a/packages/frontend/src/graphql/fragments/user.js b/packages/frontend/src/graphql/fragments/user.js index c8d73c01f..952d6fb1c 100644 --- a/packages/frontend/src/graphql/fragments/user.js +++ b/packages/frontend/src/graphql/fragments/user.js @@ -27,6 +27,7 @@ export const usersOwnInviteFieldsFragment = gql` inviteId streamId streamName + token invitedBy { ...LimitedUserFields } diff --git a/packages/frontend/src/graphql/generated/graphql.ts b/packages/frontend/src/graphql/generated/graphql.ts index 3db9e3c36..9bb21fb84 100644 --- a/packages/frontend/src/graphql/generated/graphql.ts +++ b/packages/frontend/src/graphql/generated/graphql.ts @@ -654,8 +654,8 @@ export type MutationStreamInviteCreateArgs = { export type MutationStreamInviteUseArgs = { accept: Scalars['Boolean']; - inviteId: Scalars['String']; streamId: Scalars['String']; + token: Scalars['String']; }; @@ -788,6 +788,8 @@ export type PendingStreamCollaborator = { streamName: Scalars['String']; /** E-mail address or name of the invited user */ title: Scalars['String']; + /** Only available if the active user is the pending stream collaborator */ + token?: Maybe; /** Set only if user is registered */ user?: Maybe; }; @@ -821,7 +823,7 @@ export type Query = { */ stream?: Maybe; /** - * Look for an invitation to a stream, for the current user (authed or not). If inviteId + * Look for an invitation to a stream, for the current user (authed or not). If token * isn't specified, the server will look for any valid invite. */ streamInvite?: Maybe; @@ -885,8 +887,8 @@ export type QueryStreamArgs = { export type QueryStreamInviteArgs = { - inviteId?: InputMaybe; streamId: Scalars['String']; + token?: InputMaybe; }; @@ -1186,6 +1188,8 @@ export type StreamCreateInput = { export type StreamInviteCreateInput = { email?: InputMaybe; message?: InputMaybe; + /** Defaults to the contributor role, if not specified */ + role?: InputMaybe; streamId: Scalars['String']; userId?: InputMaybe; }; @@ -1529,25 +1533,25 @@ export type LimitedUserFieldsFragment = { __typename?: 'LimitedUser', id: string export type StreamCollaboratorFieldsFragment = { __typename?: 'StreamCollaborator', id: string, name: string, role: string, company?: string | null, avatar?: string | null }; -export type UsersOwnInviteFieldsFragment = { __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }; +export type UsersOwnInviteFieldsFragment = { __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }; export type StreamInviteQueryVariables = Exact<{ streamId: Scalars['String']; - inviteId?: InputMaybe; + token?: InputMaybe; }>; -export type StreamInviteQuery = { __typename?: 'Query', streamInvite?: { __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } } | null }; +export type StreamInviteQuery = { __typename?: 'Query', streamInvite?: { __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } } | null }; export type UserStreamInvitesQueryVariables = Exact<{ [key: string]: never; }>; -export type UserStreamInvitesQuery = { __typename?: 'Query', streamInvites: Array<{ __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }> }; +export type UserStreamInvitesQuery = { __typename?: 'Query', streamInvites: Array<{ __typename?: 'PendingStreamCollaborator', id: string, inviteId: string, streamId: string, streamName: string, token?: string | null, invitedBy: { __typename?: 'LimitedUser', id: string, name?: string | null, bio?: string | null, company?: string | null, avatar?: string | null, verified?: boolean | null } }> }; export type UseStreamInviteMutationVariables = Exact<{ accept: Scalars['Boolean']; streamId: Scalars['String']; - inviteId: Scalars['String']; + token: Scalars['String']; }>; @@ -1796,6 +1800,7 @@ export const UsersOwnInviteFields = gql` inviteId streamId streamName + token invitedBy { ...LimitedUserFields } @@ -1936,8 +1941,8 @@ export const StreamCommitQuery = gql` } `; export const StreamInvite = gql` - query StreamInvite($streamId: String!, $inviteId: String) { - streamInvite(streamId: $streamId, inviteId: $inviteId) { + query StreamInvite($streamId: String!, $token: String) { + streamInvite(streamId: $streamId, token: $token) { ...UsersOwnInviteFields } } @@ -1950,8 +1955,8 @@ export const UserStreamInvites = gql` } ${UsersOwnInviteFields}`; export const UseStreamInvite = gql` - mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $inviteId: String!) { - streamInviteUse(accept: $accept, streamId: $streamId, inviteId: $inviteId) + mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $token: String!) { + streamInviteUse(accept: $accept, streamId: $streamId, token: $token) } `; export const CancelStreamInvite = gql` @@ -2340,6 +2345,7 @@ export const UsersOwnInviteFieldsFragmentDoc = gql` inviteId streamId streamName + token invitedBy { ...LimitedUserFields } @@ -2569,8 +2575,8 @@ export const useStreamCommitQueryQuery = createSmartQueryOptionsFunction< >(StreamCommitQueryDocument, handleApolloError); export const StreamInviteDocument = gql` - query StreamInvite($streamId: String!, $inviteId: String) { - streamInvite(streamId: $streamId, inviteId: $inviteId) { + query StreamInvite($streamId: String!, $token: String) { + streamInvite(streamId: $streamId, token: $token) { ...UsersOwnInviteFields } } @@ -2590,7 +2596,7 @@ export const StreamInviteDocument = gql` * streamInvite: useStreamInviteQuery({ * variables: { * streamId: // value for 'streamId' - * inviteId: // value for 'inviteId' + * token: // value for 'token' * }, * loadingKey: 'loading', * fetchPolicy: 'no-cache', @@ -2638,8 +2644,8 @@ export const useUserStreamInvitesQuery = createSmartQueryOptionsFunction< >(UserStreamInvitesDocument, handleApolloError); export const UseStreamInviteDocument = gql` - mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $inviteId: String!) { - streamInviteUse(accept: $accept, streamId: $streamId, inviteId: $inviteId) + mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $token: String!) { + streamInviteUse(accept: $accept, streamId: $streamId, token: $token) } `; @@ -2658,7 +2664,7 @@ export const UseStreamInviteDocument = gql` * variables: { * accept: // value for 'accept' * streamId: // value for 'streamId' - * inviteId: // value for 'inviteId' + * token: // value for 'token' * }, * }); */ diff --git a/packages/frontend/src/graphql/invites.js b/packages/frontend/src/graphql/invites.js index 4ed6481e6..3f76ce5c9 100644 --- a/packages/frontend/src/graphql/invites.js +++ b/packages/frontend/src/graphql/invites.js @@ -2,8 +2,8 @@ import gql from 'graphql-tag' import { usersOwnInviteFieldsFragment } from '@/graphql/fragments/user' export const streamInviteQuery = gql` - query StreamInvite($streamId: String!, $inviteId: String) { - streamInvite(streamId: $streamId, inviteId: $inviteId) { + query StreamInvite($streamId: String!, $token: String) { + streamInvite(streamId: $streamId, token: $token) { ...UsersOwnInviteFields } } @@ -22,8 +22,8 @@ export const userStreamInvitesQuery = gql` ` export const useStreamInviteMutation = gql` - mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $inviteId: String!) { - streamInviteUse(accept: $accept, streamId: $streamId, inviteId: $inviteId) + mutation UseStreamInvite($accept: Boolean!, $streamId: String!, $token: String!) { + streamInviteUse(accept: $accept, streamId: $streamId, token: $token) } ` diff --git a/packages/frontend/src/main/components/activity/ListItemActivity.vue b/packages/frontend/src/main/components/activity/ListItemActivity.vue index 190ed21ce..e98c9457c 100644 --- a/packages/frontend/src/main/components/activity/ListItemActivity.vue +++ b/packages/frontend/src/main/components/activity/ListItemActivity.vue @@ -152,7 +152,7 @@ mdi-source-commit {{ stream.commits.totalCount }} - + mdi-account-key-outline {{ stream.role.split(':')[1] }} diff --git a/packages/frontend/src/main/components/auth/AuthStrategies.vue b/packages/frontend/src/main/components/auth/AuthStrategies.vue index cef69d359..ffb51073b 100644 --- a/packages/frontend/src/main/components/auth/AuthStrategies.vue +++ b/packages/frontend/src/main/components/auth/AuthStrategies.vue @@ -19,7 +19,7 @@ :color="s.color" :href="`${s.url}?appId=${appId}&challenge=${challenge}${ suuid ? '&suuid=' + suuid : '' - }${inviteId ? '&inviteId=' + inviteId : ''}`" + }${token ? '&token=' + token : ''}`" > {{ s.icon }} {{ s.name }} @@ -30,7 +30,7 @@ ` @@ -284,7 +317,8 @@ describe('[Stream & Server Invites]', () => { email, message: unsanitaryMessage, userId: user?.id || null, - streamId: stream?.id || null + streamId: stream?.id || null, + role: role || null }) // Check that operation was successful @@ -303,7 +337,8 @@ describe('[Stream & Server Invites]', () => { expect(emailParams.html).to.not.contain(messagePart2) // Validate that invite exists - await validateInviteExistanceFromEmail(emailParams) + const invite = await validateInviteExistanceFromEmail(emailParams) + expect(invite.role).to.eq(role || Roles.Stream.Contributor) }) }) @@ -348,21 +383,24 @@ describe('[Stream & Server Invites]', () => { const serverInvite1 = { message: 'some server invite1', email: 'serverinvite1recipient@google.com', - inviteId: undefined + inviteId: undefined, + token: undefined } const streamInvite1 = { message: 'some stream invite1', email: 'somestreaminvite1recipient@google.com', stream: myPrivateStream, - inviteId: undefined + inviteId: undefined, + token: undefined } const streamInvite2 = { message: 'some stream invite2', user: otherGuy, stream: myPrivateStream, - inviteId: undefined + inviteId: undefined, + token: undefined } const invites = [serverInvite1, streamInvite1, streamInvite2] @@ -382,7 +420,10 @@ describe('[Stream & Server Invites]', () => { // Creating some invites await Promise.all( invites.map((i) => - createInviteDirectly(i, me.id).then((id) => (i.inviteId = id)) + createInviteDirectly(i, me.id).then((o) => { + i.inviteId = o.inviteId + i.token = o.token + }) ) ) }) @@ -414,18 +455,23 @@ describe('[Stream & Server Invites]', () => { { message: 'some server invite1', email: 'serverinvite1recipient@google.com', - inviteId: undefined + inviteId: undefined, + token: undefined }, { message: 'some stream invite1', email: 'somestreaminvite1recipient@google.com', stream: myPrivateStream, - inviteId: undefined + inviteId: undefined, + token: undefined } ] await Promise.all( deletableInvites.map((i) => - createInviteDirectly(i, me.id).then((id) => (i.inviteId = id)) + createInviteDirectly(i, me.id).then((o) => { + i.inviteId = o.inviteId + i.token = o.token + }) ) ) @@ -489,6 +535,12 @@ describe('[Stream & Server Invites]', () => { userId: otherGuy.id, message: 'waddup', streamId: myPrivateStream.id + }, + { + email: 'someroleguy@asdasdad.com', + message: 'yoo bruh', + streamId: myPrivateStream.id, + role: Roles.Stream.Reviewer } ] @@ -511,7 +563,9 @@ describe('[Stream & Server Invites]', () => { expect(emailParams).to.be.ok expect(emailParams.html).to.contain(inputData.message) expect(emailParams.text).to.contain(inputData.message) - await validateInviteExistanceFromEmail(emailParams) + + const invite = await validateInviteExistanceFromEmail(emailParams) + expect(invite.role).to.eq(inputData.role || Roles.Stream.Contributor) } }) }) @@ -521,26 +575,28 @@ describe('[Stream & Server Invites]', () => { message: 'some stream invite3', user: me, stream: otherGuysStream, - inviteId: undefined + inviteId: undefined, + token: undefined } beforeEach(async () => { // Create an invite before each test so that we can mutate them // in each test as needed - await createInviteDirectly(inviteFromOtherGuy, otherGuy.id).then( - (id) => (inviteFromOtherGuy.inviteId = id) - ) + await createInviteDirectly(inviteFromOtherGuy, otherGuy.id).then((o) => { + inviteFromOtherGuy.inviteId = o.inviteId + inviteFromOtherGuy.token = o.token + }) }) const inviteRetrievalDataset = [ - { display: 'by id', withId: true }, - { display: 'without an invite ID', withId: false } + { display: 'by token', withId: true }, + { display: 'without a token', withId: false } ] inviteRetrievalDataset.forEach(({ display, withId }) => { it(`the invite can be retrieved ${display}`, async () => { const result = await getStreamInvite(apollo, { streamId: inviteFromOtherGuy.stream.id, - inviteId: withId ? inviteFromOtherGuy.inviteId : null + token: withId ? inviteFromOtherGuy.token : null }) expect(result.data?.streamInvite).to.be.ok @@ -548,6 +604,7 @@ describe('[Stream & Server Invites]', () => { const data = result.data.streamInvite expect(data.inviteId).to.eq(inviteFromOtherGuy.inviteId) + expect(data.token).to.eq(inviteFromOtherGuy.token) expect(data.streamId).to.eq(inviteFromOtherGuy.stream.id) expect(data.title).to.eq(me.name) @@ -565,12 +622,13 @@ describe('[Stream & Server Invites]', () => { ] useUpDataSet.forEach(({ display, accept }) => { it(`the invite can be ${display}`, async () => { + const token = inviteFromOtherGuy.token const inviteId = inviteFromOtherGuy.inviteId const streamId = inviteFromOtherGuy.stream.id const { data, errors } = await useUpStreamInvite(apollo, { accept, - inviteId, + token, streamId }) @@ -633,19 +691,24 @@ describe('[Stream & Server Invites]', () => { // Create a couple of static invites that shouldn't be mutated in tests await Promise.all([ - createInviteDirectly(myInvite, me.id).then((id) => (myInvite.inviteId = id)), - createInviteDirectly(otherGuysInvite, otherGuy.id).then( - (id) => (otherGuysInvite.inviteId = id) - ) + createInviteDirectly(myInvite, me.id).then((o) => { + myInvite.inviteId = o.inviteId + myInvite.token = o.token + }), + createInviteDirectly(otherGuysInvite, otherGuy.id).then((o) => { + otherGuysInvite.inviteId = o.inviteId + otherGuysInvite.token = o.token + }) ]) }) beforeEach(async () => { // Create an invite before each test so that we can mutate them // in each test as needed - await createInviteDirectly(dynamicInvite, me.id).then( - (id) => (dynamicInvite.inviteId = id) - ) + await createInviteDirectly(dynamicInvite, me.id).then((o) => { + dynamicInvite.inviteId = o.inviteId + dynamicInvite.token = o.token + }) }) it('a pending invite can be deleted', async () => { @@ -670,8 +733,15 @@ describe('[Stream & Server Invites]', () => { expect(errors).to.be.not.ok expect(data.stream).to.be.ok expect(data.stream.id).to.eq(streamId) - expect(data.stream.pendingCollaborators || []).to.have.length(1) - expect(data.stream.pendingCollaborators[0].user?.id).to.eq(otherGuy.id) + + const pendingCollaborators = data.stream.pendingCollaborators || [] + expect(pendingCollaborators).to.have.length(1) + + const pendingCollaborator = pendingCollaborators[0] + expect(pendingCollaborator.user?.id).to.eq(otherGuy.id) + + // tokens shouldn't be resolved, as they're for other people + expect(pendingCollaborator.token).to.be.null }) it("a foreign stream's pending collaborators can't be retrieved", async () => { diff --git a/packages/server/test/graphql/serverInvites.js b/packages/server/test/graphql/serverInvites.js index f3051f89f..f3e2e59a1 100644 --- a/packages/server/test/graphql/serverInvites.js +++ b/packages/server/test/graphql/serverInvites.js @@ -12,7 +12,8 @@ const { gql } = require('apollo-server-express') * email: string | null, * userId: string | null, * streamId: string, - * message: string + * message: string, + * role: string | null * }} StreamInviteCreateInput */ @@ -59,6 +60,7 @@ const streamInviteFragment = gql` streamId title role + token invitedBy { id name @@ -79,8 +81,8 @@ const streamInviteFragment = gql` ` const streamInviteQuery = gql` - query ($streamId: String!, $inviteId: String) { - streamInvite(streamId: $streamId, inviteId: $inviteId) { + query ($streamId: String!, $token: String) { + streamInvite(streamId: $streamId, token: $token) { ...StreamInviteData } } @@ -99,8 +101,8 @@ const streamInvitesQuery = gql` ` const useStreamInviteMutation = gql` - mutation ($accept: Boolean!, $streamId: String!, $inviteId: String!) { - streamInviteUse(accept: $accept, streamId: $streamId, inviteId: $inviteId) + mutation ($accept: Boolean!, $streamId: String!, $token: String!) { + streamInviteUse(accept: $accept, streamId: $streamId, token: $token) } ` @@ -117,6 +119,7 @@ const streamPendingCollaboratorsQuery = gql` pendingCollaborators { inviteId title + token user { id name @@ -209,10 +212,10 @@ module.exports = { * streamInvite query * @param {import('apollo-server-express').ApolloServer} apollo */ - getStreamInvite(apollo, { streamId, inviteId }) { + getStreamInvite(apollo, { streamId, token }) { return apollo.executeOperation({ query: streamInviteQuery, - variables: { streamId, inviteId } + variables: { streamId, token } }) }, /** @@ -228,10 +231,10 @@ module.exports = { * streamInviteUse mutation * @param {import('apollo-server-express').ApolloServer} apollo */ - useUpStreamInvite(apollo, { accept, streamId, inviteId }) { + useUpStreamInvite(apollo, { accept, streamId, token }) { return apollo.executeOperation({ query: useStreamInviteMutation, - variables: { accept, streamId, inviteId } + variables: { accept, streamId, token } }) }, /** diff --git a/packages/server/test/speckle-helpers/inviteHelper.js b/packages/server/test/speckle-helpers/inviteHelper.js index b85524361..03ffa7f27 100644 --- a/packages/server/test/speckle-helpers/inviteHelper.js +++ b/packages/server/test/speckle-helpers/inviteHelper.js @@ -19,6 +19,8 @@ const { * streamId?: string * }} invite * @param {string} creatorId + * + * @returns {Promise<{inviteId: string, token: string}>} */ function createInviteDirectly(invite, creatorId) { const userId = invite.userId || invite.user?.id || null