diff --git a/packages/api/src/routers/notification_router.ts b/packages/api/src/routers/notification_router.ts new file mode 100644 index 000000000..14e44303d --- /dev/null +++ b/packages/api/src/routers/notification_router.ts @@ -0,0 +1,73 @@ +import express from 'express' +import { getDeviceTokensByUserId } from '../services/user_device_tokens' +import { + PushNotificationType, + sendMulticastPushNotifications, +} from '../utils/sendNotification' +import cors from 'cors' +import { corsConfig } from '../utils/corsConfig' +import * as jwt from 'jsonwebtoken' +import { env } from '../env' +import { Claims } from '../resolvers/types' + +interface Notification { + body: string + title?: string + data?: Record + image?: string + notificationType?: PushNotificationType +} + +export function notificationRouter() { + const router = express.Router() + + router.options('/send', cors({ ...corsConfig, maxAge: 600 })) + router.post('/send', cors(corsConfig), async (req, res) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const token = (req.cookies?.auth || req.headers?.authorization) as string + if (!token || !jwt.verify(token, env.server.jwtSecret)) { + return res.status(401).send({ errorCode: 'UNAUTHORIZED' }) + } + const claims = jwt.decode(token) as Claims + const { uid: userId } = claims + const { + body, + title, + data, + image: imageUrl, + notificationType, + } = req.body as Notification + + if (!userId || !body) { + return res.status(400).send({ errorCode: 'BAD_DATA' }) + } + + const tokens = await getDeviceTokensByUserId(userId) + if (tokens.length === 0) { + return res.status(400).send({ errorCode: 'NO_DEVICE_TOKENS' }) + } + + const message = { + notification: { + title, + body, + imageUrl, + }, + data, + tokens: tokens.map((token) => token.token), + } + + const result = await sendMulticastPushNotifications( + userId, + message, + notificationType || 'rule' + ) + if (!result) { + return res.status(400).send({ errorCode: 'SEND_NOTIFICATION_FAILED' }) + } + + res.send('OK') + }) + + return router +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 0438c6921..adae47e87 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -47,6 +47,7 @@ import { webhooksServiceRouter } from './routers/svc/webhooks' import { integrationsServiceRouter } from './routers/svc/integrations' import { textToSpeechRouter } from './routers/text_to_speech' import * as httpContext from 'express-http-context' +import { notificationRouter } from './routers/notification_router' const PORT = process.env.PORT || 4000 @@ -131,6 +132,7 @@ export const createApp = (): { app.use('/api/article', articleRouter()) app.use('/api/mobile-auth', mobileAuthRouter()) app.use('/api/text-to-speech', textToSpeechRouter()) + app.use('/api/notification', notificationRouter()) app.use('/svc/pubsub/content', contentServiceRouter()) app.use('/svc/pubsub/links', linkServiceRouter()) app.use('/svc/pubsub/newsletters', newsletterServiceRouter()) diff --git a/packages/api/src/utils/sendNotification.ts b/packages/api/src/utils/sendNotification.ts index b19299002..edbd86365 100644 --- a/packages/api/src/utils/sendNotification.ts +++ b/packages/api/src/utils/sendNotification.ts @@ -8,7 +8,7 @@ import { import { env } from '../env' import { analytics } from './analytics' -type PushNotificationType = 'newsletter' | 'reminder' +export type PushNotificationType = 'newsletter' | 'reminder' | 'rule' // getting credentials from App Engine initializeApp() diff --git a/packages/rule-handler/src/notification.ts b/packages/rule-handler/src/notification.ts index 31bfc20ca..287d9e886 100644 --- a/packages/rule-handler/src/notification.ts +++ b/packages/rule-handler/src/notification.ts @@ -1,97 +1,35 @@ -import { applicationDefault, initializeApp } from 'firebase-admin/app' -import { - BatchResponse, - getMessaging, - Message, - MulticastMessage, -} from 'firebase-admin/messaging' import axios from 'axios' import { getAuthToken } from './index' -export interface DeviceToken { - id: string - token: string - userId: string - createdAt: Date +interface NotificationData { + body: string + title?: string + data?: Record + image?: string + notificationType?: string } -// getting credentials from App Engine -initializeApp({ - credential: applicationDefault(), -}) - -export const getDeviceTokens = async ( +export const sendNotification = async ( userId: string, apiEndpoint: string, - jwtSecret: string -): Promise => { + jwtSecret: string, + message: string, + title?: string, + image?: string +) => { const auth = await getAuthToken(userId, jwtSecret) - const data = JSON.stringify({ - query: `query { - deviceTokens { - ... on DeviceTokensError { - errorCodes - } - ... on DeviceTokensSuccess { - deviceTokens { - id - token - createdAt - } - } - } - }`, - }) + const data: NotificationData = { + body: message, + title: title || message, + image, + notificationType: 'rule', + } - const response = await axios.post(`${apiEndpoint}/graphql`, data, { + await axios.post(`${apiEndpoint}/notification/send`, data, { headers: { Cookie: `auth=${auth};`, 'Content-Type': 'application/json', }, }) - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return response.data.data.deviceTokens.deviceTokens as DeviceToken[] -} - -export const getBatchMessages = ( - messages: string[], - tokens: string[], - title?: string, - imageUrl?: string -): Message[] => { - const batchMessages: Message[] = [] - messages.forEach((message) => { - tokens.forEach((token) => { - batchMessages.push({ - token, - notification: { - title, - body: message, - imageUrl, - }, - }) - }) - }) - - return batchMessages -} - -export const sendPushNotification = async ( - message: Message -): Promise => { - return getMessaging().send(message) -} - -export const sendMulticastPushNotifications = async ( - message: MulticastMessage -): Promise => { - return getMessaging().sendMulticast(message) -} - -export const sendBatchPushNotifications = async ( - messages: Message[] -): Promise => { - return getMessaging().sendAll(messages) } diff --git a/packages/rule-handler/src/rule.ts b/packages/rule-handler/src/rule.ts index 990de516d..8e84f303c 100644 --- a/packages/rule-handler/src/rule.ts +++ b/packages/rule-handler/src/rule.ts @@ -1,8 +1,4 @@ -import { - getBatchMessages, - getDeviceTokens, - sendBatchPushNotifications, -} from './notification' +import { sendNotification } from './notification' import { getAuthToken, PubSubData } from './index' import axios from 'axios' import { parse, SearchParserKeyWordOffset } from 'search-query-parser' @@ -142,33 +138,10 @@ export const triggerActions = async ( case RuleActionType.MarkAsRead: continue case RuleActionType.SendNotification: - if (action.params.length === 0) { - console.log('No notification messages provided') - continue + for (const message of action.params) { + await sendNotification(userId, apiEndpoint, jwtSecret, message) } - await sendNotification(userId, action.params, apiEndpoint, jwtSecret) } } } } - -export const sendNotification = async ( - userId: string, - messages: string[], - apiEndpoint: string, - jwtSecret: string, - title?: string, - image?: string -) => { - // get device tokens by calling api - const tokens = await getDeviceTokens(userId, apiEndpoint, jwtSecret) - - const batchMessages = getBatchMessages( - messages, - tokens.map((t) => t.token), - title, - image - ) - - return sendBatchPushNotifications(batchMessages) -}