From e9f513263f195f985d265c79348bc00715228aed Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Thu, 9 Jan 2025 13:46:36 +0100 Subject: [PATCH 1/4] feat(Mattermost): Fine grained notification settings --- .../components/ProviderList/ProviderList.tsx | 2 +- packages/client/types/modules.d.ts | 1 + ...08T13:29:16.434Z_addNotificationSettings.ts | 18 ++++++++++++++++++ .../toolboxSrc/applyEnvVarsToClientAssets.ts | 3 ++- scripts/webpack/dev.client.config.js | 4 ++-- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts diff --git a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx index b34428f0fbc..3447aadd5a2 100644 --- a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx @@ -142,7 +142,7 @@ const ProviderList = (props: Props) => { }, { name: 'Mattermost', - connected: !!integrations?.mattermost.auth, + connected: window.__ACTION__.mattermostGlobal || !!integrations?.mattermost.auth, component: }, { diff --git a/packages/client/types/modules.d.ts b/packages/client/types/modules.d.ts index dbb21cd9384..3eb9c3fd1e5 100644 --- a/packages/client/types/modules.d.ts +++ b/packages/client/types/modules.d.ts @@ -32,6 +32,7 @@ interface Window { google: string googleAnalytics: string mattermostDisabled: boolean | undefined + mattermostGlobal: boolean | undefined msTeamsDisabled: boolean | undefined publicPath: string sentry: string diff --git a/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts b/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts new file mode 100644 index 00000000000..f5e21086c5e --- /dev/null +++ b/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts @@ -0,0 +1,18 @@ +import type { Kysely } from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .createTable('NotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('service', 'IntegrationProviderServiceEnum', (col) => col.notNull()) + .addColumn('event', 'SlackNotificationEventEnum', (col) => col.notNull()) + .addColumn('teamId', 'varchar(100)', (col) => col.references('Team.id').onDelete('cascade').notNull()) + .addUniqueConstraint('NotificationSettings_type_service_teamId_key', ['service', 'event', 'teamId']) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .dropTable('NotificationSettings') + .execute() +} diff --git a/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts b/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts index 16fce913f6e..701f5d61536 100644 --- a/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts +++ b/scripts/toolboxSrc/applyEnvVarsToClientAssets.ts @@ -55,7 +55,8 @@ const rewriteIndexHTML = () => { github: process.env.GITHUB_CLIENT_ID, google: process.env.GOOGLE_OAUTH_CLIENT_ID, googleAnalytics: process.env.GA_TRACKING_ID, - mattermostDisabled: !!process.env.MATTERMOST_SECRET || process.env.MATTERMOST_DISABLED === 'true', + mattermostDisabled: process.env.MATTERMOST_DISABLED === 'true', + mattermostGlobal: !!process.env.MATTERMOST_SECRET, msTeamsDisabled: process.env.MSTEAMS_DISABLED === 'true', sentry: process.env.SENTRY_DSN, slack: process.env.SLACK_CLIENT_ID, diff --git a/scripts/webpack/dev.client.config.js b/scripts/webpack/dev.client.config.js index 934e377ce24..0490d31867f 100644 --- a/scripts/webpack/dev.client.config.js +++ b/scripts/webpack/dev.client.config.js @@ -128,8 +128,8 @@ module.exports = { github: process.env.GITHUB_CLIENT_ID, google: process.env.GOOGLE_OAUTH_CLIENT_ID, googleAnalytics: process.env.GA_TRACKING_ID, - mattermostDisabled: - !!process.env.MATTERMOST_SECRET || process.env.MATTERMOST_DISABLED === 'true', + mattermostDisabled: process.env.MATTERMOST_DISABLED === 'true', + mattermostGlobal: !!process.env.MATTERMOST_SECRET, msTeamsDisabled: process.env.MSTEAMS_DISABLED === 'true', sentry: process.env.SENTRY_DSN, slack: process.env.SLACK_CLIENT_ID, From 044fe0be782f689cbd6108529a276a1b52408628 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Thu, 16 Jan 2025 13:41:42 +0100 Subject: [PATCH 2/4] feat: Add notification settings for Mattermost and MS Teams This only includes the GraphQL types for now. --- codegen.json | 1 + .../gqlIds/TeamMemberIntegrationAuthId.ts | 8 +-- .../server/dataloader/azureDevOpsLoaders.ts | 12 ++-- packages/server/dataloader/gcalLoaders.ts | 12 ++-- packages/server/dataloader/gitlabLoaders.ts | 12 ++-- .../dataloader/integrationAuthLoaders.ts | 36 ++++++++--- .../server/dataloader/jiraServerLoaders.ts | 6 +- .../dataloader/primaryKeyLoaderMakers.ts | 8 +++ .../graphql/mutations/setTaskEstimate.ts | 2 +- .../mutations/setNotificationSetting.ts | 54 ++++++++++++++++ .../updateJiraServerDimensionField.ts | 2 +- .../graphql/public/typeDefs/Mutation.graphql | 17 +++++ .../SetNotificationSettingPayload.graphql | 4 ++ .../SetNotificationSettingSuccess.graphql | 10 +++ .../TeamMemberIntegrationAuthWebhook.graphql | 7 ++- .../typeDefs/TeamSubscriptionPayload.graphql | 1 + .../AddTeamMemberIntegrationAuthSuccess.ts | 4 +- .../public/types/AzureDevOpsIntegration.ts | 2 +- .../graphql/public/types/GitLabIntegration.ts | 2 +- .../public/types/JiraServerIntegration.ts | 10 +-- .../public/types/JiraServerRemoteProject.ts | 2 +- .../public/types/MSTeamsIntegration.ts | 4 +- .../public/types/MattermostIntegration.ts | 2 +- .../types/SetNotificationSettingSuccess.ts | 16 +++++ .../types/TeamMemberIntegrationAuthOAuth1.ts | 2 +- .../types/TeamMemberIntegrationAuthOAuth2.ts | 2 +- .../types/TeamMemberIntegrationAuthWebhook.ts | 5 +- .../TaskIntegrationManagerFactory.ts | 4 +- ...8T13:29:16.434Z_addNotificationSettings.ts | 18 ------ ...5T13:29:16.434Z_addNotificationSettings.ts | 63 +++++++++++++++++++ .../server/utils/getTaskServicesWithPerms.ts | 4 +- 31 files changed, 265 insertions(+), 67 deletions(-) create mode 100644 packages/server/graphql/public/mutations/setNotificationSetting.ts create mode 100644 packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql create mode 100644 packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql create mode 100644 packages/server/graphql/public/types/SetNotificationSettingSuccess.ts delete mode 100644 packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts create mode 100644 packages/server/postgres/migrations/2025-01-15T13:29:16.434Z_addNotificationSettings.ts diff --git a/codegen.json b/codegen.json index be3000b8483..590a8c15a35 100644 --- a/codegen.json +++ b/codegen.json @@ -161,6 +161,7 @@ "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", + "SetNotificationSettingSuccess": "./types/SetNotificationSettingSuccess#SetNotificationSettingSuccessSource", "SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource", "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", "ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource", diff --git a/packages/client/shared/gqlIds/TeamMemberIntegrationAuthId.ts b/packages/client/shared/gqlIds/TeamMemberIntegrationAuthId.ts index 854ba68eb57..02bf98fb17d 100644 --- a/packages/client/shared/gqlIds/TeamMemberIntegrationAuthId.ts +++ b/packages/client/shared/gqlIds/TeamMemberIntegrationAuthId.ts @@ -1,8 +1,8 @@ const TeamMemberIntegrationAuthId = { - join: (service: string, teamId: string, userId: string) => `${service}:${teamId}:${userId}`, - split: (id: string) => { - const [service, teamId, userId] = id.split(':') - return {service, teamId, userId} + join: (dbId: number) => `TeamMemberIntegrationAuth:${dbId}`, + split: (gqlId: string) => { + const [_, id] = gqlId.split(':') + return Number.parseInt(id!)! } } diff --git a/packages/server/dataloader/azureDevOpsLoaders.ts b/packages/server/dataloader/azureDevOpsLoaders.ts index 101004c6cf7..05eea8a72aa 100644 --- a/packages/server/dataloader/azureDevOpsLoaders.ts +++ b/packages/server/dataloader/azureDevOpsLoaders.ts @@ -138,11 +138,13 @@ export const freshAzureDevOpsAuth = ( async (keys) => { const results = await Promise.allSettled( keys.map(async ({userId, teamId}) => { - const azureDevOpsAuthToRefresh = await parent.get('teamMemberIntegrationAuths').load({ - service: 'azureDevOps', - teamId, - userId - }) + const azureDevOpsAuthToRefresh = await parent + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({ + service: 'azureDevOps', + teamId, + userId + }) if (azureDevOpsAuthToRefresh === null) { return null } diff --git a/packages/server/dataloader/gcalLoaders.ts b/packages/server/dataloader/gcalLoaders.ts index 758b7f40a56..7cc904fa0db 100644 --- a/packages/server/dataloader/gcalLoaders.ts +++ b/packages/server/dataloader/gcalLoaders.ts @@ -10,11 +10,13 @@ export const freshGcalAuth = (parent: RootDataLoader) => { async (keys) => { const results = await Promise.allSettled( keys.map(async ({teamId, userId}) => { - const gcalAuth = await parent.get('teamMemberIntegrationAuths').load({ - service: 'gcal', - teamId, - userId - }) + const gcalAuth = await parent + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({ + service: 'gcal', + teamId, + userId + }) if (!gcalAuth) return null const {expiresAt} = gcalAuth const now = new Date() diff --git a/packages/server/dataloader/gitlabLoaders.ts b/packages/server/dataloader/gitlabLoaders.ts index 68435fd7245..95dc6ce4beb 100644 --- a/packages/server/dataloader/gitlabLoaders.ts +++ b/packages/server/dataloader/gitlabLoaders.ts @@ -10,11 +10,13 @@ export const freshGitlabAuth = (parent: RootDataLoader) => { async (keys) => { const results = await Promise.allSettled( keys.map(async ({teamId, userId}) => { - const gitlabAuth = await parent.get('teamMemberIntegrationAuths').load({ - service: 'gitlab', - teamId, - userId - }) + const gitlabAuth = await parent + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({ + service: 'gitlab', + teamId, + userId + }) if (!gitlabAuth) return null const {expiresAt} = gitlabAuth const now = new Date() diff --git a/packages/server/dataloader/integrationAuthLoaders.ts b/packages/server/dataloader/integrationAuthLoaders.ts index 69433e278e6..7948e1691fe 100644 --- a/packages/server/dataloader/integrationAuthLoaders.ts +++ b/packages/server/dataloader/integrationAuthLoaders.ts @@ -1,5 +1,4 @@ import DataLoader from 'dataloader' -import TeamMemberIntegrationAuthId from '../../client/shared/gqlIds/TeamMemberIntegrationAuthId' import errorFilter from '../graphql/errorFilter' import isValid from '../graphql/isValid' import getKysely from '../postgres/getKysely' @@ -11,10 +10,11 @@ import getIntegrationProvidersByIds, { } from '../postgres/queries/getIntegrationProvidersByIds' import {selectSlackNotifications, selectTeamMemberIntegrationAuth} from '../postgres/select' import {SlackAuth, SlackNotification, TeamMemberIntegrationAuth} from '../postgres/types' +import {NotificationSettings} from '../postgres/types/pg' import NullableDataLoader from './NullableDataLoader' import RootDataLoader from './RootDataLoader' -interface TeamMemberIntegrationAuthPrimaryKey { +interface TeamMemberIntegrationAuthServiceTeamUserKey { service: IntegrationProviderServiceEnum teamId: string userId: string @@ -32,7 +32,7 @@ const teamMemberIntegrationAuthCacheKeyFn = ({ service, teamId, userId -}: TeamMemberIntegrationAuthPrimaryKey) => TeamMemberIntegrationAuthId.join(service, teamId, userId) +}: TeamMemberIntegrationAuthServiceTeamUserKey) => `${service}-${teamId}-${userId}` export const integrationProviders = (parent: RootDataLoader) => { return new NullableDataLoader( @@ -88,7 +88,11 @@ export const sharedIntegrationProviders = (parent: RootDataLoader) => { } export const bestTeamIntegrationProviders = (parent: RootDataLoader) => { - return new DataLoader( + return new DataLoader< + TeamMemberIntegrationAuthServiceTeamUserKey, + TIntegrationProvider | null, + string + >( async (keys) => { // given token params, get the best team token const bestTeamIntegrationAuths = ( @@ -118,9 +122,9 @@ export const bestTeamIntegrationProviders = (parent: RootDataLoader) => { ) } -export const teamMemberIntegrationAuths = (parent: RootDataLoader) => { +export const teamMemberIntegrationAuthsByServiceTeamAndUserId = (parent: RootDataLoader) => { return new DataLoader< - TeamMemberIntegrationAuthPrimaryKey, + TeamMemberIntegrationAuthServiceTeamUserKey, TeamMemberIntegrationAuth | null, string >( @@ -152,7 +156,7 @@ export const teamMemberIntegrationAuths = (parent: RootDataLoader) => { export const bestTeamIntegrationAuths = (parent: RootDataLoader) => { return new DataLoader< - TeamMemberIntegrationAuthPrimaryKey, + TeamMemberIntegrationAuthServiceTeamUserKey, IGetBestTeamIntegrationAuthQueryResult | null, string >( @@ -247,3 +251,21 @@ export const teamMemberIntegrationAuthsByTeamId = (parent: RootDataLoader) => { } ) } + +export const notificationSettingsByAuthId = (parent: RootDataLoader) => { + return new DataLoader( + async (keys) => { + const pg = getKysely() + const res = await pg + .selectFrom('NotificationSettings') + .selectAll() + .where(({eb}) => eb('authId', 'in', keys)) + .execute() + + return keys.map((key) => res.filter(({authId}) => authId === key).map(({event}) => event)) + }, + { + ...parent.dataLoaderOptions + } + ) +} diff --git a/packages/server/dataloader/jiraServerLoaders.ts b/packages/server/dataloader/jiraServerLoaders.ts index 0cd151234fe..ea091c7e794 100644 --- a/packages/server/dataloader/jiraServerLoaders.ts +++ b/packages/server/dataloader/jiraServerLoaders.ts @@ -65,7 +65,7 @@ export const jiraServerIssue = (parent: RootDataLoader) => { const results = await Promise.allSettled( keys.map(async ({teamId, userId, issueId}) => { const auth = await parent - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { @@ -110,7 +110,7 @@ export const allJiraServerProjects = (parent: RootDataLoader) => { return Promise.all( keys.map(async ({userId, teamId}) => { const auth = await parent - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) return [] const provider = await parent.get('integrationProviders').loadNonNull(auth.providerId) @@ -140,7 +140,7 @@ export const jiraServerFieldTypes = (parent: RootDataLoader) => return Promise.all( keys.map(async ({teamId, userId, projectId, issueType}) => { const auth = await parent - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index ad797541829..c4680eddfd3 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -156,3 +156,11 @@ export const tasks = primaryKeyLoaderMaker((ids: readonly string[]) => { export const notifications = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectNotifications().where('id', 'in', ids).execute() }) + +export const teamMemberIntegrationAuths = primaryKeyLoaderMaker((ids: readonly number[]) => { + return getKysely() + .selectFrom('TeamMemberIntegrationAuth') + .selectAll() + .where('id', 'in', ids) + .execute() +}) diff --git a/packages/server/graphql/mutations/setTaskEstimate.ts b/packages/server/graphql/mutations/setTaskEstimate.ts index b89c64e6f4e..2c1849fbb85 100644 --- a/packages/server/graphql/mutations/setTaskEstimate.ts +++ b/packages/server/graphql/mutations/setTaskEstimate.ts @@ -212,7 +212,7 @@ const setTaskEstimate = { const [auth /*, team*/] = await Promise.all([ dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId: accessUserId}), dataLoader.get('teams').load(teamId) ]) diff --git a/packages/server/graphql/public/mutations/setNotificationSetting.ts b/packages/server/graphql/public/mutations/setNotificationSetting.ts new file mode 100644 index 00000000000..cd79ed2a4b4 --- /dev/null +++ b/packages/server/graphql/public/mutations/setNotificationSetting.ts @@ -0,0 +1,54 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import TeamMemberIntegrationAuthId from '../../../../client/shared/gqlIds/TeamMemberIntegrationAuthId' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const setNotificationSetting: MutationResolvers['setNotificationSetting'] = async ( + _source, + {authId: gqlAuthId, event, isEnabled}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const pg = getKysely() + + // AUTH + const authId = TeamMemberIntegrationAuthId.split(gqlAuthId) + const auth = await dataLoader.get('teamMemberIntegrationAuths').load(authId) + if (!auth) { + return standardError(new Error('Integration auth not found'), {userId: viewerId}) + } + const {teamId, service} = auth + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) + } + + // VALIDATION + if (service !== 'mattermost' && service !== 'msTeams') { + return standardError(new Error('Invalid integration provider'), {userId: viewerId}) + } + + // RESOLUTION + if (isEnabled) { + await pg + .insertInto('NotificationSettings') + .values({authId, event}) + .onConflict((oc) => oc.doNothing()) + .execute() + } else { + await pg + .deleteFrom('NotificationSettings') + .where('authId', '=', authId) + .where('event', '=', event) + .execute() + } + const data = {authId} + publish(SubscriptionChannel.TEAM, teamId, 'SetNotificationSettingSuccess', data, subOptions) + return data +} + +export default setNotificationSetting diff --git a/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts b/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts index 2443adc0121..863c0f8095c 100644 --- a/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts +++ b/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts @@ -38,7 +38,7 @@ const updateJiraServerDimensionField: MutationResolvers['updateJiraServerDimensi const data = {teamId, meetingId} const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId: viewerId}) if (!auth) { return {error: {message: 'Not authenticated with JiraServer'}} diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index a38790ed86f..5b898ded486 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -787,12 +787,29 @@ type Mutation { """ timeRemaining: Float ): SetStageTimerPayload! + setSlackNotification( slackChannelId: ID slackNotificationEvents: [SlackNotificationEventEnum!]! teamId: ID! ): SetSlackNotificationPayload! + """ + Set the notification settings for a provider and team + """ + setNotificationSetting( + """ + ID of the TeamMemberIntegrationAuth for which to set the notification setting + """ + authId: ID! + + """ + Event type to modify + """ + event: SlackNotificationEventEnum! + isEnabled: Boolean! + ): SetNotificationSettingPayload! + """ Broadcast that the viewer started dragging a reflection """ diff --git a/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql b/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql new file mode 100644 index 00000000000..5c691a798c0 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql @@ -0,0 +1,4 @@ +""" +Return for setNotificationSetting mutation +""" +union SetNotificationSettingPayload = ErrorPayload | SetNotificationSettingSuccess diff --git a/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql b/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql new file mode 100644 index 00000000000..5b004a7201a --- /dev/null +++ b/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql @@ -0,0 +1,10 @@ +type SetNotificationSettingSuccess { + authId: ID! + + auth: TeamMemberIntegrationAuth! + + """ + Enabled events for this provider and team + """ + events: [SlackNotificationEventEnum!]! +} diff --git a/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql b/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql index 8d9d8b98a08..17bac55ca03 100644 --- a/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql +++ b/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql @@ -1,5 +1,5 @@ """ -An integration authorization that connects via Webhook auth strategy +A notification integration authorization that connects via Webhook auth strategy """ type TeamMemberIntegrationAuthWebhook implements TeamMemberIntegrationAuth { """ @@ -41,4 +41,9 @@ type TeamMemberIntegrationAuthWebhook implements TeamMemberIntegrationAuth { The provider strategy this token connects to """ provider: IntegrationProviderWebhook! + + """ + The events that trigger a notification + """ + events: [SlackNotificationEventEnum!]! } diff --git a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql index 5d988e06788..9e9988d7d2f 100644 --- a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql +++ b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql @@ -22,6 +22,7 @@ type TeamSubscriptionPayload { RemoveTeamMemberPayload: RemoveTeamMemberPayload RenameMeetingSuccess: RenameMeetingSuccess SelectTemplatePayload: SelectTemplatePayload + SetNotificationSettingSuccess: SetNotificationSettingSuccess StartCheckInSuccess: StartCheckInSuccess StartRetrospectiveSuccess: StartRetrospectiveSuccess StartSprintPokerSuccess: StartSprintPokerSuccess diff --git a/packages/server/graphql/public/types/AddTeamMemberIntegrationAuthSuccess.ts b/packages/server/graphql/public/types/AddTeamMemberIntegrationAuthSuccess.ts index 5e0c14bd641..b8ff456bfe2 100644 --- a/packages/server/graphql/public/types/AddTeamMemberIntegrationAuthSuccess.ts +++ b/packages/server/graphql/public/types/AddTeamMemberIntegrationAuthSuccess.ts @@ -9,7 +9,9 @@ export type AddTeamMemberIntegrationAuthSuccessSource = { const AddTeamMemberIntegrationAuthSuccess: AddTeamMemberIntegrationAuthSuccessResolvers = { integrationAuth: async ({service, teamId, userId}, _args, {dataLoader}) => { - return (await dataLoader.get('teamMemberIntegrationAuths').load({service, teamId, userId}))! + return (await dataLoader + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({service, teamId, userId}))! }, teamMember: ({teamId, userId}, _args, {dataLoader}) => { const teamMemberId = toTeamMemberId(teamId, userId) diff --git a/packages/server/graphql/public/types/AzureDevOpsIntegration.ts b/packages/server/graphql/public/types/AzureDevOpsIntegration.ts index 1514fc81ce3..765b6d45b61 100644 --- a/packages/server/graphql/public/types/AzureDevOpsIntegration.ts +++ b/packages/server/graphql/public/types/AzureDevOpsIntegration.ts @@ -18,7 +18,7 @@ type WorkItemArgs = { const AzureDevOpsIntegration: AzureDevOpsIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { return dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'azureDevOps', teamId, userId}) }, diff --git a/packages/server/graphql/public/types/GitLabIntegration.ts b/packages/server/graphql/public/types/GitLabIntegration.ts index 14d7052f067..74f47c269f4 100644 --- a/packages/server/graphql/public/types/GitLabIntegration.ts +++ b/packages/server/graphql/public/types/GitLabIntegration.ts @@ -54,7 +54,7 @@ const GitLabIntegration: GitLabIntegrationResolvers = { const {dataLoader} = context const emptyConnection = {edges: [], pageInfo: {hasNextPage: false, hasPreviousPage: false}} const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'gitlab', teamId, userId}) if (!auth?.accessToken) return emptyConnection const {providerId} = auth diff --git a/packages/server/graphql/public/types/JiraServerIntegration.ts b/packages/server/graphql/public/types/JiraServerIntegration.ts index 21fc0a4b54b..4f0198c8c17 100644 --- a/packages/server/graphql/public/types/JiraServerIntegration.ts +++ b/packages/server/graphql/public/types/JiraServerIntegration.ts @@ -23,7 +23,7 @@ type IssueArgs = { const JiraServerIntegration: JiraServerIntegrationResolvers = { id: async ({teamId, userId}, _args, {dataLoader}) => { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { @@ -35,7 +35,7 @@ const JiraServerIntegration: JiraServerIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) return auth! }, @@ -68,7 +68,7 @@ const JiraServerIntegration: JiraServerIntegrationResolvers = { } const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { @@ -159,7 +159,7 @@ const JiraServerIntegration: JiraServerIntegrationResolvers = { providerId: async ({teamId, userId}, _args, {dataLoader}) => { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { @@ -171,7 +171,7 @@ const JiraServerIntegration: JiraServerIntegrationResolvers = { searchQueries: async ({teamId, userId}, _args, {dataLoader}) => { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { diff --git a/packages/server/graphql/public/types/JiraServerRemoteProject.ts b/packages/server/graphql/public/types/JiraServerRemoteProject.ts index 14ad9a4f6f8..0da030117e9 100644 --- a/packages/server/graphql/public/types/JiraServerRemoteProject.ts +++ b/packages/server/graphql/public/types/JiraServerRemoteProject.ts @@ -12,7 +12,7 @@ const JiraServerRemoteProject: JiraServerRemoteProjectResolvers = { avatar: async ({avatarUrls, teamId, userId}, _args, {dataLoader}) => { const url = avatarUrls['48x48'] const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) return defaultJiraProjectAvatar const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) diff --git a/packages/server/graphql/public/types/MSTeamsIntegration.ts b/packages/server/graphql/public/types/MSTeamsIntegration.ts index 1a64daeef66..9b434ed29fe 100644 --- a/packages/server/graphql/public/types/MSTeamsIntegration.ts +++ b/packages/server/graphql/public/types/MSTeamsIntegration.ts @@ -7,7 +7,9 @@ export type MSTeamsIntegrationSource = { const MSTeamsIntegration: MsTeamsIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { - return dataLoader.get('teamMemberIntegrationAuths').load({service: 'msTeams', teamId, userId}) + return dataLoader + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({service: 'msTeams', teamId, userId}) }, sharedProviders: async ({teamId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/MattermostIntegration.ts b/packages/server/graphql/public/types/MattermostIntegration.ts index fcc4c05f4b6..9bbc3521e9a 100644 --- a/packages/server/graphql/public/types/MattermostIntegration.ts +++ b/packages/server/graphql/public/types/MattermostIntegration.ts @@ -8,7 +8,7 @@ export type MattermostIntegrationSource = { const MattermostIntegration: MattermostIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { const res = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'mattermost', teamId, userId}) return res! }, diff --git a/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts b/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts new file mode 100644 index 00000000000..c799bac1cc0 --- /dev/null +++ b/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts @@ -0,0 +1,16 @@ +import {SetNotificationSettingSuccessResolvers} from '../resolverTypes' + +export type SetNotificationSettingSuccessSource = { + authId: number +} + +const SetNotificationSettingSuccess: SetNotificationSettingSuccessResolvers = { + auth: async ({authId}, _args, {dataLoader}) => { + return dataLoader.get('teamMemberIntegrationAuths').loadNonNull(authId) + }, + events: async ({authId}, _args, {dataLoader}) => { + return dataLoader.get('notificationSettingsByAuthId').load(authId) + } +} + +export default SetNotificationSettingSuccess diff --git a/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth1.ts b/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth1.ts index 81216f24b99..714371cca47 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth1.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth1.ts @@ -10,7 +10,7 @@ const TeamMemberIntegrationAuthOAuth1: TeamMemberIntegrationAuthOAuth1Resolvers accessToken: string | undefined | null accessTokenSecret: string | undefined | null }) => !!(accessToken && accessTokenSecret), - id: ({service, teamId, userId}) => TeamMemberIntegrationAuthId.join(service, teamId, userId), + id: ({id}) => TeamMemberIntegrationAuthId.join(id), providerId: ({providerId}) => IntegrationProviderId.join(providerId), provider: async ({providerId}, _args, {dataLoader}) => { return dataLoader.get('integrationProviders').loadNonNull(providerId) diff --git a/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth2.ts b/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth2.ts index 079c958ce9f..31272ab5928 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth2.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrationAuthOAuth2.ts @@ -4,7 +4,7 @@ import {TeamMemberIntegrationAuthOAuth2Resolvers} from '../resolverTypes' const TeamMemberIntegrationAuthOAuth2: TeamMemberIntegrationAuthOAuth2Resolvers = { __isTypeOf: ({accessToken, refreshToken, scopes}) => !!(accessToken && refreshToken && scopes), - id: ({service, teamId, userId}) => TeamMemberIntegrationAuthId.join(service, teamId, userId), + id: ({id}) => TeamMemberIntegrationAuthId.join(id), providerId: ({providerId}) => IntegrationProviderId.join(providerId), provider: async ({providerId}, _args, {dataLoader}) => { return dataLoader.get('integrationProviders').loadNonNull(providerId) diff --git a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts index 33248cc8a5a..fe7b1e72ab5 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts @@ -4,10 +4,13 @@ import {TeamMemberIntegrationAuthWebhookResolvers} from '../resolverTypes' const TeamMemberIntegrationAuthWebhook: TeamMemberIntegrationAuthWebhookResolvers = { __isTypeOf: ({accessToken, refreshToken, scopes}) => !accessToken || !refreshToken || !scopes, - id: ({service, teamId, userId}) => TeamMemberIntegrationAuthId.join(service, teamId, userId), + id: ({id}) => TeamMemberIntegrationAuthId.join(id), providerId: ({providerId}) => IntegrationProviderId.join(providerId), provider: async ({providerId}, _args, {dataLoader}) => { return dataLoader.get('integrationProviders').loadNonNull(providerId) + }, + events: async ({id}, _args, {dataLoader}) => { + return dataLoader.get('notificationSettingsByAuthId').load(id) } } diff --git a/packages/server/integrations/TaskIntegrationManagerFactory.ts b/packages/server/integrations/TaskIntegrationManagerFactory.ts index a535c686931..7b7ded03244 100644 --- a/packages/server/integrations/TaskIntegrationManagerFactory.ts +++ b/packages/server/integrations/TaskIntegrationManagerFactory.ts @@ -79,7 +79,7 @@ export default class TaskIntegrationManagerFactory { if (service === 'jiraServer') { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'jiraServer', teamId, userId}) if (!auth) { @@ -92,7 +92,7 @@ export default class TaskIntegrationManagerFactory { if (service === 'azureDevOps') { const auth = await dataLoader - .get('teamMemberIntegrationAuths') + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') .load({service: 'azureDevOps', teamId, userId}) if (!auth) { diff --git a/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts b/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts deleted file mode 100644 index f5e21086c5e..00000000000 --- a/packages/server/postgres/migrations/2025-01-08T13:29:16.434Z_addNotificationSettings.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Kysely } from 'kysely' - -export async function up(db: Kysely): Promise { - await db.schema - .createTable('NotificationSettings') - .addColumn('id', 'serial', (col) => col.primaryKey()) - .addColumn('service', 'IntegrationProviderServiceEnum', (col) => col.notNull()) - .addColumn('event', 'SlackNotificationEventEnum', (col) => col.notNull()) - .addColumn('teamId', 'varchar(100)', (col) => col.references('Team.id').onDelete('cascade').notNull()) - .addUniqueConstraint('NotificationSettings_type_service_teamId_key', ['service', 'event', 'teamId']) - .execute() -} - -export async function down(db: Kysely): Promise { - await db.schema - .dropTable('NotificationSettings') - .execute() -} diff --git a/packages/server/postgres/migrations/2025-01-15T13:29:16.434Z_addNotificationSettings.ts b/packages/server/postgres/migrations/2025-01-15T13:29:16.434Z_addNotificationSettings.ts new file mode 100644 index 00000000000..96f68c3c428 --- /dev/null +++ b/packages/server/postgres/migrations/2025-01-15T13:29:16.434Z_addNotificationSettings.ts @@ -0,0 +1,63 @@ +import {sql, type Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .addUniqueConstraint('TeamMemberIntegrationAuth_userId_teamId_service_uniqu', [ + 'userId', + 'teamId', + 'service' + ]) + .execute() + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .dropConstraint('TeamMemberIntegrationAuth_pkey') + .execute() + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .execute() + + await db.schema + .createTable('NotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('authId', 'integer', (col) => + col.references('TeamMemberIntegrationAuth.id').onDelete('cascade').notNull() + ) + .addColumn('event', sql`"SlackNotificationEventEnum"`, (col) => col.notNull()) + .addUniqueConstraint('NotificationSettings_authId_event_key', ['authId', 'event']) + .execute() + + await db.schema + .createIndex('NotificationSettings_authId_idx') + .on('NotificationSettings') + .column('authId') + .execute() + + await db + .insertInto('NotificationSettings') + .columns(['authId', 'event']) + .expression((eb) => + eb + .selectFrom('TeamMemberIntegrationAuth') + .select(['id', sql`unnest(enum_range(NULL::"SlackNotificationEventEnum"))`]) + .where((eb) => eb.or([eb('service', '=', 'mattermost'), eb('service', '=', 'msTeams')])) + ) + .execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable('NotificationSettings').execute() + + await db.schema.alterTable('TeamMemberIntegrationAuth').dropColumn('id').execute() + + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .addPrimaryKeyConstraint('TeamMemberIntegrationAuth_pkey', ['userId', 'teamId', 'service']) + .execute() + + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .dropConstraint('TeamMemberIntegrationAuth_userId_teamId_service_unique') + .execute() +} diff --git a/packages/server/utils/getTaskServicesWithPerms.ts b/packages/server/utils/getTaskServicesWithPerms.ts index 09066c629b4..6ea97dad38a 100644 --- a/packages/server/utils/getTaskServicesWithPerms.ts +++ b/packages/server/utils/getTaskServicesWithPerms.ts @@ -13,7 +13,9 @@ const getTaskServicesWithPerms = async ( dataLoader.get('githubAuth').load({teamId, userId}), dataLoader.get('freshGitlabAuth').load({teamId, userId}), dataLoader.get('freshAzureDevOpsAuth').load({teamId, userId}), - dataLoader.get('teamMemberIntegrationAuths').load({service: 'jiraServer', teamId, userId}) + dataLoader + .get('teamMemberIntegrationAuthsByServiceTeamAndUserId') + .load({service: 'jiraServer', teamId, userId}) ]) const allPossibleTaskServices = IntegrationProviderServiceEnum.getValues().map( ({name}) => name From 627f9c5795b60906a6dbe2795655bf5e7d665e3e Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 20 Jan 2025 14:52:28 +0100 Subject: [PATCH 3/4] Honor NotificationSettings in MSTeamsNotifier and MattermostNotifier --- .../dataloader/integrationAuthLoaders.ts | 76 +++---------------- .../helpers/notifications/MSTeamsNotifier.ts | 33 +++++--- .../notifications/MattermostNotifier.ts | 39 +++------- .../mutations/addTeamMemberIntegrationAuth.ts | 46 +++++++++-- ..._removeTeamMemberIntegrationAuthChannel.ts | 12 +++ .../queries/getBestTeamIntegrationAuth.ts | 14 ---- .../src/getBestTeamIntegrationAuthQuery.sql | 9 --- 7 files changed, 93 insertions(+), 136 deletions(-) create mode 100644 packages/server/postgres/migrations/2025-01-20T10:49:02.535Z_removeTeamMemberIntegrationAuthChannel.ts delete mode 100644 packages/server/postgres/queries/getBestTeamIntegrationAuth.ts delete mode 100644 packages/server/postgres/queries/src/getBestTeamIntegrationAuthQuery.sql diff --git a/packages/server/dataloader/integrationAuthLoaders.ts b/packages/server/dataloader/integrationAuthLoaders.ts index 7948e1691fe..9099b920886 100644 --- a/packages/server/dataloader/integrationAuthLoaders.ts +++ b/packages/server/dataloader/integrationAuthLoaders.ts @@ -2,9 +2,7 @@ import DataLoader from 'dataloader' import errorFilter from '../graphql/errorFilter' import isValid from '../graphql/isValid' import getKysely from '../postgres/getKysely' -import {IGetBestTeamIntegrationAuthQueryResult} from '../postgres/queries/generated/getBestTeamIntegrationAuthQuery' import {IntegrationProviderServiceEnum} from '../postgres/queries/generated/getIntegrationProvidersByIdsQuery' -import getBestTeamIntegrationAuth from '../postgres/queries/getBestTeamIntegrationAuth' import getIntegrationProvidersByIds, { TIntegrationProvider } from '../postgres/queries/getIntegrationProvidersByIds' @@ -87,41 +85,6 @@ export const sharedIntegrationProviders = (parent: RootDataLoader) => { ) } -export const bestTeamIntegrationProviders = (parent: RootDataLoader) => { - return new DataLoader< - TeamMemberIntegrationAuthServiceTeamUserKey, - TIntegrationProvider | null, - string - >( - async (keys) => { - // given token params, get the best team token - const bestTeamIntegrationAuths = ( - await parent.get('bestTeamIntegrationAuths').loadMany(keys) - ).filter(isValid) - // dedupe providerIds - const providerIds = Array.from( - new Set(bestTeamIntegrationAuths.map((token) => token.providerId)) - ) - // get the providers for each token - const integrationProviders = ( - await parent.get('integrationProviders').loadMany(providerIds) - ).filter(isValid) - return keys.map((key) => { - const token = bestTeamIntegrationAuths.find( - ({service, teamId}) => service === key.service && teamId === key.teamId - ) - if (!token) return null - const provider = integrationProviders.find(({id}) => id === token.providerId) - return provider ?? null - }) - }, - { - ...parent.dataLoaderOptions, - cacheKeyFn: teamMemberIntegrationAuthCacheKeyFn - } - ) -} - export const teamMemberIntegrationAuthsByServiceTeamAndUserId = (parent: RootDataLoader) => { return new DataLoader< TeamMemberIntegrationAuthServiceTeamUserKey, @@ -154,29 +117,6 @@ export const teamMemberIntegrationAuthsByServiceTeamAndUserId = (parent: RootDat ) } -export const bestTeamIntegrationAuths = (parent: RootDataLoader) => { - return new DataLoader< - TeamMemberIntegrationAuthServiceTeamUserKey, - IGetBestTeamIntegrationAuthQueryResult | null, - string - >( - async (keys) => { - // TODO check the teamMemberIntegrationAuths loader first, it probably exists there & then we don't have to hit the DB - const results = await Promise.allSettled( - keys.map(async ({service, teamId, userId}) => - getBestTeamIntegrationAuth(service, teamId, userId) - ) - ) - const vals = results.map((result) => (result.status === 'fulfilled' ? result.value : null)) - return vals - }, - { - ...parent.dataLoaderOptions, - cacheKeyFn: teamMemberIntegrationAuthCacheKeyFn - } - ) -} - interface TeamNotificationEvent { teamId: string event: SlackNotification['event'] @@ -224,21 +164,25 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => { }) } -export const teamMemberIntegrationAuthsByTeamId = (parent: RootDataLoader) => { +export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoader) => { return new DataLoader< - {teamId: string; service: IntegrationProviderServiceEnum}, + {teamId: string; service: IntegrationProviderServiceEnum; event: SlackNotification['event']}, TeamMemberIntegrationAuth[], string >( async (keys) => { const pg = getKysely() - const teamIds = keys.map(({teamId}) => teamId) - const services = keys.map(({service}) => service) const res = (await pg .selectFrom('TeamMemberIntegrationAuth') + .innerJoin('NotificationSettings', 'authId', 'TeamMemberIntegrationAuth.id') .selectAll() - .where(({eb}) => eb('teamId', 'in', teamIds)) - .where(({eb}) => eb('service', 'in', services)) + .where(({eb, refTuple, tuple}) => + eb( + refTuple('teamId', 'service', 'event'), + 'in', + keys.map(({teamId, service, event}) => tuple(teamId, service, event)) + ) + ) .execute()) as unknown as TeamMemberIntegrationAuth[] return keys.map((key) => diff --git a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts index 45459007256..3172da054f8 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts @@ -7,6 +7,7 @@ import {IntegrationProviderMSTeams as IIntegrationProviderMSTeams} from '../../. import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' +import {NotificationSettings} from '../../../../postgres/types/pg' import MSTeamsServerManager from '../../../../utils/MSTeamsServerManager' import {analytics} from '../../../../utils/analytics/analytics' import sendToSentry from '../../../../utils/sendToSentry' @@ -333,20 +334,28 @@ export const MSTeamsNotificationHelper: NotificationIntegrationHelper { + const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) + return MSTeamsNotificationHelper({ + ...(provider as IntegrationProviderMSTeams), + userId, + email: user.email + }) + }) + ) } export const MSTeamsNotifier = createNotifier(getMSTeams) diff --git a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts index a90e6141055..7de510ad66c 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts @@ -5,9 +5,10 @@ import findStageById from 'parabol-client/utils/meetings/findStageById' import {phaseLabelLookup} from 'parabol-client/utils/meetings/lookups' import appOrigin from '../../../../appOrigin' import {IntegrationProviderMattermost} from '../../../../postgres/queries/getIntegrationProvidersByIds' -import {SlackNotification, Team, TeamMemberIntegrationAuth} from '../../../../postgres/types' +import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' +import {NotificationSettings} from '../../../../postgres/types/pg' import MattermostServerManager from '../../../../utils/MattermostServerManager' import {analytics} from '../../../../utils/analytics/analytics' import {toEpochSeconds} from '../../../../utils/epochTime' @@ -97,7 +98,6 @@ const makeEndMeetingButtons = (meeting: AnyMeeting) => { type MattermostNotificationAuth = { userId: string teamId: string - channel: string | null webhookUrl: string | null serverBaseUrl: string | null sharedSecret: string | null @@ -352,7 +352,12 @@ const MattermostNotificationHelper: NotificationIntegrationHelper { - if (auth.channel) { - if (!acc.some((a) => a.channel === auth.channel)) { - acc.push(auth) - } - } - return acc - }, [] as TeamMemberIntegrationAuth[]) - if (filteredAuths.length === 0) { - const webhookAuth = - auths.find((auth) => auth.userId === userId) ?? auths.filter((auth) => !auth.channel)[0] - if (webhookAuth) { - filteredAuths.push(webhookAuth) - } - } + .get('teamMemberIntegrationAuthsByTeamIdAndEvent') + .load({service: 'mattermost', teamId, event}) return Promise.all( - filteredAuths.map(async (auth) => { + auths.map(async (auth) => { const provider = (await dataLoader .get('integrationProviders') .loadNonNull(auth.providerId)) as IntegrationProviderMattermost - return MattermostNotificationHelper({...provider, teamId, userId, channel: auth.channel}) + return MattermostNotificationHelper({...provider, teamId, userId}) }) ) } diff --git a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts index 93282714c12..28a86ee7ffb 100644 --- a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts +++ b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts @@ -1,11 +1,12 @@ +import {sql} from 'kysely' import IntegrationProviderId from '~/shared/gqlIds/IntegrationProviderId' import GcalOAuth2Manager from '../../../integrations/gcal/GcalOAuth2Manager' import GitLabOAuth2Manager from '../../../integrations/gitlab/GitLabOAuth2Manager' import JiraServerOAuth1Manager, { OAuth1Auth } from '../../../integrations/jiraServer/JiraServerOAuth1Manager' +import getKysely from '../../../postgres/getKysely' import {IntegrationProviderAzureDevOps} from '../../../postgres/queries/getIntegrationProvidersByIds' -import upsertTeamMemberIntegrationAuth from '../../../postgres/queries/upsertTeamMemberIntegrationAuth' import AzureDevOpsServerManager from '../../../utils/AzureDevOpsServerManager' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -27,6 +28,7 @@ const addTeamMemberIntegrationAuth: MutationResolvers['addTeamMemberIntegrationA ) => { const {authToken, dataLoader} = context const viewerId = getUserId(authToken) + const pg = getKysely() //AUTH if (!isTeamMember(authToken, teamId)) { @@ -128,13 +130,41 @@ const addTeamMemberIntegrationAuth: MutationResolvers['addTeamMemberIntegrationA } // RESOLUTION - await upsertTeamMemberIntegrationAuth({ - ...tokenMetadata, - providerId: providerDbId, - service, - teamId, - userId: viewerId - }) + const auth = await pg + .insertInto('TeamMemberIntegrationAuth') + .values({ + ...tokenMetadata, + providerId: providerDbId, + service, + teamId, + userId: viewerId + }) + .onConflict((oc) => + oc.columns(['userId', 'teamId', 'service']).doUpdateSet({ + ...tokenMetadata, + providerId: providerDbId, + isActive: true + }) + ) + .returning('id') + .executeTakeFirst() + const authId = auth?.id + if (!authId) { + return standardError(new Error('Failed to insert TeamMemberIntegrationAuth'), { + userId: viewerId + }) + } + + await pg + .insertInto('NotificationSettings') + .columns(['authId', 'event']) + .values(() => ({ + authId, + event: sql`unnest(enum_range(NULL::"SlackNotificationEventEnum"))` + })) + .onConflict((oc) => oc.doNothing()) + .execute() + updateRepoIntegrationsCacheByPerms(dataLoader, viewerId, teamId, true) analytics.integrationAdded(viewer, teamId, service) diff --git a/packages/server/postgres/migrations/2025-01-20T10:49:02.535Z_removeTeamMemberIntegrationAuthChannel.ts b/packages/server/postgres/migrations/2025-01-20T10:49:02.535Z_removeTeamMemberIntegrationAuthChannel.ts new file mode 100644 index 00000000000..d2fbea8a037 --- /dev/null +++ b/packages/server/postgres/migrations/2025-01-20T10:49:02.535Z_removeTeamMemberIntegrationAuthChannel.ts @@ -0,0 +1,12 @@ +import type {Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('TeamMemberIntegrationAuth').dropColumn('channel').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable('TeamMemberIntegrationAuth') + .addColumn('channel', 'varchar(255)') + .execute() +} diff --git a/packages/server/postgres/queries/getBestTeamIntegrationAuth.ts b/packages/server/postgres/queries/getBestTeamIntegrationAuth.ts deleted file mode 100644 index e7c9f4d2622..00000000000 --- a/packages/server/postgres/queries/getBestTeamIntegrationAuth.ts +++ /dev/null @@ -1,14 +0,0 @@ -import getPg from '../getPg' -import {getBestTeamIntegrationAuthQuery} from './generated/getBestTeamIntegrationAuthQuery' -import {IntegrationProviderServiceEnum} from './generated/getIntegrationProvidersByIdsQuery' - -const getBestTeamIntegrationAuth = async ( - service: IntegrationProviderServiceEnum, - teamId: string, - userId: string -) => { - const [res] = await getBestTeamIntegrationAuthQuery.run({service, teamId, userId}, getPg()) - return res ?? null -} - -export default getBestTeamIntegrationAuth diff --git a/packages/server/postgres/queries/src/getBestTeamIntegrationAuthQuery.sql b/packages/server/postgres/queries/src/getBestTeamIntegrationAuthQuery.sql deleted file mode 100644 index ba51e294f05..00000000000 --- a/packages/server/postgres/queries/src/getBestTeamIntegrationAuthQuery.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - @name getBestTeamIntegrationAuthQuery - */ -SELECT *, "userId" = :userId as "isUser" FROM "TeamMemberIntegrationAuth" -WHERE "teamId" = :teamId -AND "service" = :service -AND "isActive" = TRUE -ORDER BY "isUser" DESC, "updatedAt" DESC -LIMIT 1; From ff9500a4eec901a2deb963e5d46fa1bc2cb3aeeb Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 20 Jan 2025 15:03:46 +0100 Subject: [PATCH 4/4] Hide Mattermost settings when global provider is used The settings don't work for it yet. --- .../teamDashboard/components/ProviderList/ProviderList.tsx | 2 +- .../components/ProviderRow/MattermostProviderRow.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx index 3447aadd5a2..b34428f0fbc 100644 --- a/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderList/ProviderList.tsx @@ -142,7 +142,7 @@ const ProviderList = (props: Props) => { }, { name: 'Mattermost', - connected: window.__ACTION__.mattermostGlobal || !!integrations?.mattermost.auth, + connected: !!integrations?.mattermost.auth, component: }, { diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx index 45a66d6e195..89b3f4bd7d6 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx @@ -52,7 +52,7 @@ const MattermostProviderRow = (props: Props) => { const {mattermost} = integrations const {auth} = mattermost - if (window.__ACTION__.mattermostDisabled) return null + if (window.__ACTION__.mattermostGlobal || window.__ACTION__.mattermostDisabled) return null return ( <>