From c715e72b61efa3c20553a5865047da8b03ca0766 Mon Sep 17 00:00:00 2001 From: Lomy Date: Tue, 25 Jul 2023 15:44:41 +0200 Subject: [PATCH 1/8] add controller --- .../chatNotificationController/index.ts | 71 +++++++++++++++++++ web/server.ts | 3 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 web/controllers/chatNotificationController/index.ts diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts new file mode 100644 index 000000000..513735761 --- /dev/null +++ b/web/controllers/chatNotificationController/index.ts @@ -0,0 +1,71 @@ +import { getLogger } from '../../../common/logger/logger'; +import { Request, Response, Router } from 'express'; + +const logger = getLogger('ChatNotification'); + +export const chatNotificationRouter = Router(); + +chatNotificationRouter.post('/chat-notification', handleChatNotification); + +async function handleChatNotification(req: Request, res: Response) { + try { + logger.info('Request at /chat-notification'); + logger.info(req.body); + + // TODO get right user + + // TODO send notification to user + + res.status(200).send({ status: 'ok' }); + } catch (e) { + res.status(500).send({ error: 'Intrnal Server Error' }); + } +} + +// { +// createdAt: 1690292045626, +// data: { +// conversation: { +// createdAt: 1690285437967, +// custom: [Object], +// id: '2e3f123...', +// participants: [Object], +// photoUrl: null, +// subject: null, +// topicId: null, +// welcomeMessages: null +// }, +// messages: [ [Object], [Object] ], +// notificationId: 'ntf_6KNyKH2LFZpvbev9bQdgvR', +// recipient: { +// availabilityText: null, +// createdAt: 1690285437850, +// custom: {}, +// email: [Array], +// id: 'student_1', +// locale: null, +// name: 'Leon Jackson', +// phone: null, +// photoUrl: null, +// pushTokens: {}, +// role: 'student', +// welcomeMessage: null +// }, +// sender: { +// availabilityText: null, +// createdAt: 1690285435466, +// custom: {}, +// email: [Array], +// id: 'pupil_1', +// locale: null, +// name: 'Max M.', +// phone: null, +// photoUrl: null, +// pushTokens: {}, +// role: 'pupil', +// welcomeMessage: null +// } +// }, +// id: 'evt_AY3QOPr99lYlEemrbs', +// type: 'notification.triggered' +// } {} diff --git a/web/server.ts b/web/server.ts index c4951cdc7..3e1bd2083 100644 --- a/web/server.ts +++ b/web/server.ts @@ -17,6 +17,7 @@ import { fileRouter } from './controllers/fileController'; import { attachmentRouter } from './controllers/attachmentController'; import { certificateRouter } from './controllers/certificateController'; import { convertCertificateLinkToApiLink } from '../common/certificate'; +import { chatNotificationRouter } from './controllers/chatNotificationController'; // ------------------ Setup Logging, Common Headers, Routes ---------------- @@ -84,7 +85,6 @@ export const server = (async function setupWebserver() { app.use(cors(options)); - // ------------------------ GraphQL --------------------------- await apolloServer.start(); apolloServer.applyMiddleware({ app, path: '/apollo' }); @@ -93,6 +93,7 @@ export const server = (async function setupWebserver() { app.use('/api/attachments', attachmentRouter); app.use('/api/certificate', certificateRouter); app.use('/api/files', fileRouter); + app.use('/api', chatNotificationRouter); app.get('/:certificateId', (req, res, next) => { if (!req.subdomains.includes('verify')) { From 7b853685ef14c64f86e5bf8682501be8e499de6f Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 26 Jul 2023 13:53:44 +0200 Subject: [PATCH 2/8] add notification --- common/chat/helper.ts | 6 ++ common/notification/actions.ts | 25 ++++++ .../chatNotificationController/index.ts | 87 +++++++------------ .../chatNotificationController/types.ts | 87 +++++++++++++++++++ .../chatNotificationController/util.ts | 77 ++++++++++++++++ 5 files changed, 226 insertions(+), 56 deletions(-) create mode 100644 web/controllers/chatNotificationController/types.ts create mode 100644 web/controllers/chatNotificationController/util.ts diff --git a/common/chat/helper.ts b/common/chat/helper.ts index 0bb17e58b..f59d535c6 100644 --- a/common/chat/helper.ts +++ b/common/chat/helper.ts @@ -12,10 +12,15 @@ import { ChatAccess, ChatMetaData, Conversation, ConversationInfos, TJConversati import { MatchContactPupil, MatchContactStudent } from './contacts'; type TalkJSUserId = `${'pupil' | 'student'}_${number}`; +export type UserId = `${'pupil' | 'student'}/${number}`; const userIdToTalkJsId = (userId: string): TalkJSUserId => { return userId.replace('/', '_') as TalkJSUserId; }; + +const talkJsIdToUserId = (userId: string): UserId => { + return userId.replace('_', '/') as UserId; +}; const createChatSignature = async (user: User): Promise => { const userId = (await getOrCreateChatUser(user)).id; const key = process.env.TALKJS_API_KEY; @@ -201,6 +206,7 @@ const isPupilContact = (contact: MatchContactPupil | MatchContactStudent): conta export { userIdToTalkJsId, + talkJsIdToUserId, parseUnderscoreToSlash, checkResponseStatus, createChatSignature, diff --git a/common/notification/actions.ts b/common/notification/actions.ts index 226eaec9c..06f26485c 100644 --- a/common/notification/actions.ts +++ b/common/notification/actions.ts @@ -412,6 +412,31 @@ const _notificationActions = { appointment: sampleAppointment, }, }, + missed_one_on_one_chat_message: { + description: 'Missed message in 1:1 chat', + sampleContext: { + sender: sampleUser, + recipient: sampleUser, + conversationId: sampleAppointment, + message: '', + totalUnread: '', + matchId: '', + loginToken: '', + }, + }, + missed_course_chat_message: { + description: 'Missed message in group chat', + sampleContext: { + sender: sampleUser, + recipient: sampleUser, + conversationId: sampleAppointment, + message: '', + totalUnread: '', + courseId: '', + courseName: '', + loginToken: '', + }, + }, user_authenticate: DEPRECATED, user_login_email: DEPRECATED, diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index 513735761..854e4566d 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -1,5 +1,11 @@ import { getLogger } from '../../../common/logger/logger'; import { Request, Response, Router } from 'express'; +import { NotificationTriggered } from './types'; +import { talkJsIdToUserId } from '../../../common/chat/helper'; +import { getPupil, getStudent, getUser } from '../../../common/user'; +import * as Notification from '../../../common/notification'; +import { pupil as Pupil, student as Student } from '@prisma/client'; +import { ChatType, getChatType, getNotificationContext, verifyChatUser } from './util'; const logger = getLogger('ChatNotification'); @@ -7,65 +13,34 @@ export const chatNotificationRouter = Router(); chatNotificationRouter.post('/chat-notification', handleChatNotification); -async function handleChatNotification(req: Request, res: Response) { +async function handleChatNotification(req: Request, res: Response): Promise { try { logger.info('Request at /chat-notification'); - logger.info(req.body); - - // TODO get right user - - // TODO send notification to user - + const notificationBody: NotificationTriggered = req.body; + const { data } = notificationBody; + const recipient = data.recipient; + const recipientUserId = talkJsIdToUserId(recipient.id); + const recipientUser = await getUser(recipientUserId); + + const conversationParticipants = Object.keys(data.conversation.participants); + const chatType = getChatType(conversationParticipants); + + const isUserVerified = await verifyChatUser(recipientUser); + + if (isUserVerified) { + const notificationContext = await getNotificationContext(notificationBody); + let userToNotify: Pupil | Student; + if (recipientUser.pupilId) { + userToNotify = await getPupil(recipientUser); + } else { + userToNotify = await getStudent(recipientUser); + } + + const notificationAction = chatType === ChatType.ONE_ON_ONE ? 'missed_one_on_one_chat_message' : 'missed_course_chat_message'; + await Notification.actionTaken(userToNotify, notificationAction, notificationContext); + } res.status(200).send({ status: 'ok' }); } catch (e) { - res.status(500).send({ error: 'Intrnal Server Error' }); + res.status(500).send({ error: 'Internal Server Error' }); } } - -// { -// createdAt: 1690292045626, -// data: { -// conversation: { -// createdAt: 1690285437967, -// custom: [Object], -// id: '2e3f123...', -// participants: [Object], -// photoUrl: null, -// subject: null, -// topicId: null, -// welcomeMessages: null -// }, -// messages: [ [Object], [Object] ], -// notificationId: 'ntf_6KNyKH2LFZpvbev9bQdgvR', -// recipient: { -// availabilityText: null, -// createdAt: 1690285437850, -// custom: {}, -// email: [Array], -// id: 'student_1', -// locale: null, -// name: 'Leon Jackson', -// phone: null, -// photoUrl: null, -// pushTokens: {}, -// role: 'student', -// welcomeMessage: null -// }, -// sender: { -// availabilityText: null, -// createdAt: 1690285435466, -// custom: {}, -// email: [Array], -// id: 'pupil_1', -// locale: null, -// name: 'Max M.', -// phone: null, -// photoUrl: null, -// pushTokens: {}, -// role: 'pupil', -// welcomeMessage: null -// } -// }, -// id: 'evt_AY3QOPr99lYlEemrbs', -// type: 'notification.triggered' -// } {} diff --git a/web/controllers/chatNotificationController/types.ts b/web/controllers/chatNotificationController/types.ts new file mode 100644 index 000000000..4e5e41b47 --- /dev/null +++ b/web/controllers/chatNotificationController/types.ts @@ -0,0 +1,87 @@ +import { TJConversation } from '../../../common/chat/types'; + +type Role = 'student' | 'pupil'; + +export type NotificationTriggered = { + id: string; + createdAt: string; + data: { + notificationId: string; + recipient: Recipient; + sender: Sender; + conversation: TJConversation; + messages: UserMessage[]; + }; + type: 'notification.triggered'; +}; + +export interface Recipient { + availabilityText: string | null; + createdAt: number; + custom: Record; + email: string[]; + id: string; + locale: string | null; + name: string; + phone: string | null; + photoUrl: string | null; + pushTokens: Record; + role: Role; + welcomeMessage: string | null; +} + +export interface Sender { + availabilityText: string | null; + createdAt: number; + custom: Record; + email: string[]; + id: string; + locale: string | null; + name: string; + phone: string | null; + photoUrl: string | null; + pushTokens: Record; + role: Role; + welcomeMessage: string | null; +} + +export interface UserMessage { + attachment: any | null; + conversationId: string; + createdAt: number; + custom: Record; + editedAt: number | null; + id: string; + location: any | null; + origin: string; + readBy: string[]; + referencedMessageId: string | null; + senderId: string; + text: string; + type: string; +} + +type ChatParticipant = { + firstname?: string; + fullname?: string; +}; + +type NotificationContextBase = { + sender: ChatParticipant; + recipient: ChatParticipant; + conversationId: string; + message: string; + totalUnread: string; + loginToken?: string; +}; + +export type GroupNotificationContext = NotificationContextBase & { + courseId: string; + courseName: string; +}; + +export type OneOnOneNotificationContext = NotificationContextBase & { + matchId?: string; + courseId?: string; + subcourseIds?: string; +}; diff --git a/web/controllers/chatNotificationController/util.ts b/web/controllers/chatNotificationController/util.ts new file mode 100644 index 000000000..85eea3e29 --- /dev/null +++ b/web/controllers/chatNotificationController/util.ts @@ -0,0 +1,77 @@ +import { getChatUser } from '../../../common/chat'; +import { getLogger } from '../../../common/logger/logger'; +import { User } from '../../../common/user'; +import { getCourse, getSubcourse } from '../../../graphql/util'; +import { GroupNotificationContext, NotificationTriggered, OneOnOneNotificationContext } from './types'; + +const logger = getLogger('ChatNotification'); + +export enum ChatType { + GROUP = 'group', + ONE_ON_ONE = 'one_on_one', +} + +export function getChatType(participants: string[]) { + if (participants.length === 2) { + return ChatType.ONE_ON_ONE; + } + return ChatType.GROUP; +} + +export async function verifyChatUser(user: User) { + const chatUser = await getChatUser(user); + if (chatUser) { + return true; + } + return false; +} + +export async function getNotificationContext(notificationBody: NotificationTriggered): Promise { + const { recipient, sender, conversation, messages } = notificationBody.data; + const firstnameRecipient: string = recipient.name.split(' ')[0]; + const firstnameSender: string = sender.name.split(' ')[0]; + + const subcourseConversation = conversation.custom.subcourse ? JSON.parse(conversation.custom.subcourse) : undefined; + const match = conversation.custom.match ? JSON.parse(conversation.custom.match) : undefined; + const participants = Object.keys(conversation.participants); + + const chatType = getChatType(participants); + + let notificationContext: GroupNotificationContext | OneOnOneNotificationContext; + + if (chatType === ChatType.ONE_ON_ONE) { + let courseId: number; + if (subcourseConversation?.length === 1) { + const subcourse = await getSubcourse(subcourseConversation[0]); + courseId = subcourse.courseId; + } + + notificationContext = { + sender: { fullname: firstnameSender }, + recipient: { firstname: firstnameRecipient }, + conversationId: conversation.id, + message: messages[0].text, + totalUnread: messages.length.toString(), + ...(match ? { matchId: match.matchId } : {}), + ...(courseId ? { courseId: courseId.toString() } : {}), + ...(subcourseConversation && subcourseConversation.length > 1 ? { subcourseIds: subcourseConversation.toString() } : {}), + }; + } else if (chatType === ChatType.GROUP && subcourseConversation) { + const subcourse = await getSubcourse(subcourseConversation[0]); + const course = await getCourse(subcourse.courseId); + + notificationContext = { + sender: { fullname: firstnameSender }, + recipient: { firstname: firstnameRecipient }, + conversationId: conversation.id, + message: messages[0].text, + totalUnread: messages.length.toString(), + courseId: subcourse.courseId.toString(), + courseName: course.name, + }; + } + + logger.info(`CONTEXT: ${JSON.stringify(notificationContext)}`); + + return notificationContext; +} From bf4d929536d433a6b2028a9ae1fd510764b15452 Mon Sep 17 00:00:00 2001 From: Lomy Date: Wed, 26 Jul 2023 13:59:45 +0200 Subject: [PATCH 3/8] change sender prop in context --- web/controllers/chatNotificationController/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/controllers/chatNotificationController/util.ts b/web/controllers/chatNotificationController/util.ts index 85eea3e29..d0feae398 100644 --- a/web/controllers/chatNotificationController/util.ts +++ b/web/controllers/chatNotificationController/util.ts @@ -47,7 +47,7 @@ export async function getNotificationContext(notificationBody: NotificationTrigg } notificationContext = { - sender: { fullname: firstnameSender }, + sender: { firstname: firstnameSender }, recipient: { firstname: firstnameRecipient }, conversationId: conversation.id, message: messages[0].text, @@ -61,7 +61,7 @@ export async function getNotificationContext(notificationBody: NotificationTrigg const course = await getCourse(subcourse.courseId); notificationContext = { - sender: { fullname: firstnameSender }, + sender: { firstname: firstnameSender }, recipient: { firstname: firstnameRecipient }, conversationId: conversation.id, message: messages[0].text, From dae2d32fb58db9da33a402fd31ada097f13f7761 Mon Sep 17 00:00:00 2001 From: Lomy Date: Thu, 27 Jul 2023 08:25:35 +0200 Subject: [PATCH 4/8] review changes --- common/notification/actions.ts | 36 +++++++++++-------- .../chatNotificationController/index.ts | 5 +-- .../chatNotificationController/types.ts | 1 - .../chatNotificationController/util.ts | 7 ++-- web/server.ts | 2 +- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/common/notification/actions.ts b/common/notification/actions.ts index 06f26485c..952d3f652 100644 --- a/common/notification/actions.ts +++ b/common/notification/actions.ts @@ -43,6 +43,23 @@ const sampleAppointment = { }, }; +const sampleMissedCourseMessage = { + sender: 'Leon', + conversationId: '1', + message: 'Hey', + totalUnread: '1', + courseId: '1', + courseName: 'Gen Z', +}; + +const sampleMissedOneOnOneMessage = { + sender: 'Max', + conversationId: '2', + message: 'Hey!', + totalUnread: '1', + matchId: '1', +}; + const DEPRECATED = { description: 'DEPRECATED - DO NOT USE', }; @@ -415,26 +432,15 @@ const _notificationActions = { missed_one_on_one_chat_message: { description: 'Missed message in 1:1 chat', sampleContext: { - sender: sampleUser, - recipient: sampleUser, - conversationId: sampleAppointment, - message: '', - totalUnread: '', - matchId: '', - loginToken: '', + user: sampleUser, + message: sampleMissedOneOnOneMessage, }, }, missed_course_chat_message: { description: 'Missed message in group chat', sampleContext: { - sender: sampleUser, - recipient: sampleUser, - conversationId: sampleAppointment, - message: '', - totalUnread: '', - courseId: '', - courseName: '', - loginToken: '', + user: sampleUser, + message: sampleMissedCourseMessage, }, }, diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index 854e4566d..551b10515 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -15,7 +15,7 @@ chatNotificationRouter.post('/chat-notification', handleChatNotification); async function handleChatNotification(req: Request, res: Response): Promise { try { - logger.info('Request at /chat-notification'); + logger.info('Request at /chat/chat-notification'); const notificationBody: NotificationTriggered = req.body; const { data } = notificationBody; const recipient = data.recipient; @@ -40,7 +40,8 @@ async function handleChatNotification(req: Request, res: Response): Promise { - const { recipient, sender, conversation, messages } = notificationBody.data; - const firstnameRecipient: string = recipient.name.split(' ')[0]; + const { sender, conversation, messages } = notificationBody.data; const firstnameSender: string = sender.name.split(' ')[0]; const subcourseConversation = conversation.custom.subcourse ? JSON.parse(conversation.custom.subcourse) : undefined; @@ -48,7 +47,6 @@ export async function getNotificationContext(notificationBody: NotificationTrigg notificationContext = { sender: { firstname: firstnameSender }, - recipient: { firstname: firstnameRecipient }, conversationId: conversation.id, message: messages[0].text, totalUnread: messages.length.toString(), @@ -62,7 +60,6 @@ export async function getNotificationContext(notificationBody: NotificationTrigg notificationContext = { sender: { firstname: firstnameSender }, - recipient: { firstname: firstnameRecipient }, conversationId: conversation.id, message: messages[0].text, totalUnread: messages.length.toString(), @@ -71,7 +68,7 @@ export async function getNotificationContext(notificationBody: NotificationTrigg }; } - logger.info(`CONTEXT: ${JSON.stringify(notificationContext)}`); + logger.info('Created Notification context for chat message', notificationContext); return notificationContext; } diff --git a/web/server.ts b/web/server.ts index 3e1bd2083..4e30de0a6 100644 --- a/web/server.ts +++ b/web/server.ts @@ -93,7 +93,7 @@ export const server = (async function setupWebserver() { app.use('/api/attachments', attachmentRouter); app.use('/api/certificate', certificateRouter); app.use('/api/files', fileRouter); - app.use('/api', chatNotificationRouter); + app.use('/api/chat', chatNotificationRouter); app.get('/:certificateId', (req, res, next) => { if (!req.subdomains.includes('verify')) { From e9671d0b62f68b29c95e26d396e75f852ca309a7 Mon Sep 17 00:00:00 2001 From: Lomy Date: Thu, 27 Jul 2023 10:01:36 +0200 Subject: [PATCH 5/8] add authorization of talkjs --- web/controllers/chatNotificationController/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index 551b10515..5159ae701 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -16,6 +16,12 @@ chatNotificationRouter.post('/chat-notification', handleChatNotification); async function handleChatNotification(req: Request, res: Response): Promise { try { logger.info('Request at /chat/chat-notification'); + const userAgent = req.headers['user-agent']; + + if (!userAgent && !userAgent.includes('talkjs/webhook')) { + throw new Error('Invalid User-Agent'); + } + const notificationBody: NotificationTriggered = req.body; const { data } = notificationBody; const recipient = data.recipient; From e4711ec7812bbfaeb4fbe2494f52cce7c35b5053 Mon Sep 17 00:00:00 2001 From: Lomy Date: Thu, 27 Jul 2023 12:11:02 +0200 Subject: [PATCH 6/8] draft: authorization --- .../chatNotificationController/index.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index 5159ae701..5213dfe90 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -6,6 +6,7 @@ import { getPupil, getStudent, getUser } from '../../../common/user'; import * as Notification from '../../../common/notification'; import { pupil as Pupil, student as Student } from '@prisma/client'; import { ChatType, getChatType, getNotificationContext, verifyChatUser } from './util'; +import { createHmac } from 'crypto'; const logger = getLogger('ChatNotification'); @@ -13,14 +14,24 @@ export const chatNotificationRouter = Router(); chatNotificationRouter.post('/chat-notification', handleChatNotification); +const secretKey = process.env.TALKJS_API_KEY; async function handleChatNotification(req: Request, res: Response): Promise { try { logger.info('Request at /chat/chat-notification'); - const userAgent = req.headers['user-agent']; - if (!userAgent && !userAgent.includes('talkjs/webhook')) { - throw new Error('Invalid User-Agent'); - } + const receivedSignature = req.headers['x-talkjs-signature']; + const timestamp = req.headers['x-talkjs-timestamp']; + + const payload = timestamp + '.' + req.body; + const hash = createHmac('sha256', secretKey).update(payload); + const validSignature = hash.digest('hex').toUpperCase(); + + // logger.info(`rec sig: ${receivedSignature} : val: ${validSignature}`); + // logger.info(`body: ${JSON.stringify(req.body)} timestamp: ${timestamp}`); + + // if (receivedSignature !== validSignature) { + // throw new Error('Invalid Signature'); + // } const notificationBody: NotificationTriggered = req.body; const { data } = notificationBody; @@ -47,6 +58,7 @@ async function handleChatNotification(req: Request, res: Response): Promise Date: Thu, 27 Jul 2023 13:03:05 +0200 Subject: [PATCH 7/8] add authorization --- .../chatNotificationController/index.ts | 22 +++++++++---------- .../chatNotificationController/types.ts | 2 ++ .../chatNotificationController/util.ts | 2 ++ web/server.ts | 11 ++++++++-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index 5213dfe90..ee4d70887 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -1,11 +1,11 @@ import { getLogger } from '../../../common/logger/logger'; import { Request, Response, Router } from 'express'; -import { NotificationTriggered } from './types'; +import { NotificationTriggered, WithRawBody } from './types'; import { talkJsIdToUserId } from '../../../common/chat/helper'; import { getPupil, getStudent, getUser } from '../../../common/user'; import * as Notification from '../../../common/notification'; import { pupil as Pupil, student as Student } from '@prisma/client'; -import { ChatType, getChatType, getNotificationContext, verifyChatUser } from './util'; +import { ChatType, InvalidSignatureError, getChatType, getNotificationContext, verifyChatUser } from './util'; import { createHmac } from 'crypto'; const logger = getLogger('ChatNotification'); @@ -15,23 +15,20 @@ export const chatNotificationRouter = Router(); chatNotificationRouter.post('/chat-notification', handleChatNotification); const secretKey = process.env.TALKJS_API_KEY; -async function handleChatNotification(req: Request, res: Response): Promise { +async function handleChatNotification(req: WithRawBody, res: Response): Promise { try { logger.info('Request at /chat/chat-notification'); const receivedSignature = req.headers['x-talkjs-signature']; const timestamp = req.headers['x-talkjs-timestamp']; - const payload = timestamp + '.' + req.body; + const payload = timestamp + '.' + req.rawBody; const hash = createHmac('sha256', secretKey).update(payload); const validSignature = hash.digest('hex').toUpperCase(); - // logger.info(`rec sig: ${receivedSignature} : val: ${validSignature}`); - // logger.info(`body: ${JSON.stringify(req.body)} timestamp: ${timestamp}`); - - // if (receivedSignature !== validSignature) { - // throw new Error('Invalid Signature'); - // } + if (receivedSignature !== validSignature) { + throw new InvalidSignatureError('Invalid Signature'); + } const notificationBody: NotificationTriggered = req.body; const { data } = notificationBody; @@ -58,7 +55,10 @@ async function handleChatNotification(req: Request, res: Response): Promise = T & { rawBody: Buffer }; diff --git a/web/controllers/chatNotificationController/util.ts b/web/controllers/chatNotificationController/util.ts index 518b8477a..e1afa7c2c 100644 --- a/web/controllers/chatNotificationController/util.ts +++ b/web/controllers/chatNotificationController/util.ts @@ -72,3 +72,5 @@ export async function getNotificationContext(notificationBody: NotificationTrigg return notificationContext; } + +export class InvalidSignatureError extends Error {} diff --git a/web/server.ts b/web/server.ts index 4e30de0a6..96a25d8c2 100644 --- a/web/server.ts +++ b/web/server.ts @@ -1,5 +1,5 @@ import express from 'express'; -import http from 'http'; +import http, { IncomingMessage } from 'http'; import bodyParser from 'body-parser'; import helmet from 'helmet'; import cors from 'cors'; @@ -18,6 +18,7 @@ import { attachmentRouter } from './controllers/attachmentController'; import { certificateRouter } from './controllers/certificateController'; import { convertCertificateLinkToApiLink } from '../common/certificate'; import { chatNotificationRouter } from './controllers/chatNotificationController'; +import { WithRawBody } from './controllers/chatNotificationController/types'; // ------------------ Setup Logging, Common Headers, Routes ---------------- @@ -43,7 +44,13 @@ export const server = (async function setupWebserver() { }); // Parse Cookies and JSON Bodies: - app.use(bodyParser.json()); + app.use( + bodyParser.json({ + verify: (req: WithRawBody, res, buf) => { + req.rawBody = buf; + }, + }) + ); app.use(cookieParser()); // Add a Favicon to the Backend (as we have some URLs that are directly opened on the backend) From 0d12dd78fd7ba0b7c4af6854a17d32cf44a99af7 Mon Sep 17 00:00:00 2001 From: Lomy Date: Fri, 28 Jul 2023 14:33:32 +0200 Subject: [PATCH 8/8] review changes --- web/controllers/chatNotificationController/index.ts | 1 + web/server.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/web/controllers/chatNotificationController/index.ts b/web/controllers/chatNotificationController/index.ts index ee4d70887..1e3280159 100644 --- a/web/controllers/chatNotificationController/index.ts +++ b/web/controllers/chatNotificationController/index.ts @@ -58,6 +58,7 @@ async function handleChatNotification(req: WithRawBody, res: Response): if (error instanceof InvalidSignatureError) { logger.info('Invalid Signature'); res.status(401).send({ error: 'Unauthorized' }); + return; } logger.error(`Failed to send notification for missed messages`, error); res.status(500).send({ error: 'Internal Server Error' }); diff --git a/web/server.ts b/web/server.ts index 96a25d8c2..c8d6df751 100644 --- a/web/server.ts +++ b/web/server.ts @@ -46,6 +46,7 @@ export const server = (async function setupWebserver() { // Parse Cookies and JSON Bodies: app.use( bodyParser.json({ + // To be able to persist the raw body of the request we use the verify function and extend the request object by the key `rawBody` verify: (req: WithRawBody, res, buf) => { req.rawBody = buf; },