From b18b2ba6e4228b0837e90e137ca59e52cf24b702 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Thu, 9 Nov 2023 09:03:57 -0500 Subject: [PATCH 1/4] add identify user method --- src/__tests__/index.test.ts | 25 +++++++++++++++++ src/index.ts | 36 ++++++++++++++++++++----- src/interfaces.ts | 54 +++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index e11f23c..f29dadd 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -11,6 +11,7 @@ import { SetUserPreferencesRequest, User } from '../interfaces'; +import { createHmac } from 'crypto'; const axiosMock = new MockAdapter(axios); const restoreConsole = mockConsole(); @@ -502,3 +503,27 @@ describe('setUserPreferences with subNotificationId', () => { ); }); }); +describe('Identify user', () => { + const userEndPointRegex = /.*\/users\/.*/; + const clientId = 'testClientId_identify_user'; + const clientSecret = 'testClientSecret_identify_user'; + const userId = 'testUserId_identify_user'; + const user = { id: userId }; + test('makes API calls with a correct request body', async () => { + axiosMock.onPost(userEndPointRegex).reply(200); + await notificationapi.init(clientId, clientSecret); + await notificationapi.identifyUser({ id: userId }); + expect(axiosMock.history.post).toHaveLength(1); + expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(user)); + expect(axiosMock.history.post[0].url).toEqual( + `https://api.notificationapi.com/${clientId}/users/${userId}` + ); + expect(axiosMock.history.post[0].headers.Authorization).toEqual( + `Basic ${Buffer.from( + `${clientId}:${userId}:${createHmac('sha256', clientSecret) + .update(`${userId}`) + .digest('base64')}` + ).toString('base64')}` + ); + }); +}); diff --git a/src/index.ts b/src/index.ts index e620d22..98f07bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,11 @@ import { InitConfiguration, RetractRequest, SendRequest, - SetUserPreferencesRequest + SetUserPreferencesRequest, + User } from './interfaces'; import axios, { AxiosResponse, Method } from 'axios'; +import { createHmac } from 'crypto'; const DEFAULT_BASE_URL = 'https://api.notificationapi.com'; @@ -37,6 +39,25 @@ class NotificationAPI { this.clientId = clientId; this.clientSecret = clientSecret; }; + /** used to identify your user */ + identifyUser = async (user: User): Promise => { + const hashedUserId = `${createHmac('sha256', this.clientSecret as string) + .update(user.id) + .digest('base64')}`; + + const customAuthorization = + 'Basic ' + + Buffer.from(`${this.clientId}:${user.id}:${hashedUserId}`).toString( + 'base64' + ); + + return this.request( + 'POST', + `users/${encodeURIComponent(user.id)}`, + user, + customAuthorization + ); + }; /** Used to send a notification to the specified user. */ send = async (sendRequest: SendRequest): Promise => { return this.request('POST', 'sender', sendRequest); @@ -77,19 +98,20 @@ class NotificationAPI { request = async ( method: Method, uri: string, - data?: unknown + data?: unknown, + customAuthorization?: string ): Promise => { + const authorization: string = + customAuthorization ?? + 'Basic ' + + Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64'); try { const res = await axios.request({ method, url: `${this.baseURL}/${this.clientId}/${uri}`, data, headers: { - Authorization: - 'Basic ' + - Buffer.from(`${this.clientId}:${this.clientSecret}`).toString( - 'base64' - ) + Authorization: authorization } }); if (res.status === 202) { diff --git a/src/interfaces.ts b/src/interfaces.ts index d627bd7..f137bd6 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -5,6 +5,10 @@ export interface User { email?: string; /** Required for SMS/CALL notifications, otherwise optional. Valid format: +15005550006. Unformatted US/Canada numbers are also accepted, e.g., (415) 555-1212, 415-555-1212, or 4155551212.*/ number?: string; + /**Tokens which are required to send mobile push notifications, a user can have multiple devices and a push token is required for each device*/ + pushTokens?: PushToken[]; + /**Tokens which are required to send web push notification, a user can have multiple devices and a web push token is required for each device */ + webPushTokens?: WebPushToken[]; } export interface SendRequest { @@ -95,3 +99,53 @@ export interface InitConfiguration { /** To updated the based url. Optional.*/ baseURL?: string; } + +export interface PushToken { + /**[apn | fcm] The provider token is to be associated with. */ + type: PushProviders; + /**The full token string. */ + token: string; + /**Information about the device the token is associated with. */ + device: Device; +} + +export enum PushProviders { + /**firebase-fcm token provider */ + FCM = 'FCM', + /**APN token provider */ + APN = 'APN' +} + +export interface Device { + /**Id of the application the token is used for */ + app_id?: string; + /**Id of the advertising identifier */ + ad_id?: string; + /**Id of the device the token is associated with */ + device_id: string; + /**The device platform i.e. android, ios*/ + platform?: string; + /**The device manufacturer */ + manufacturer?: string; + /**The device model */ + model?: string; +} + +/** + * Configuration for a Push Subscription. This can be obtained on the frontend by calling + * serviceWorkerRegistration.pushManager.subscribe(). + * The expected format is the same output as JSON.stringify'ing a PushSubscription in the browser. + */ +export interface PushSubscription { + /**a string value containing the endpoint associated with the push subscription. */ + endpoint: string; + keys: { + /**An Elliptic curve Diffie–Hellman public key on the P-256 curve (that is, the NIST secp256r1 elliptic curve). The resulting key is an uncompressed point in ANSI X9.62 format. */ + p256dh: string; + /**An authentication secret, as described in Message Encryption for Web Push. */ + auth: string; + }; +} +export interface WebPushToken { + sub: PushSubscription; +} From 75e30de5ab1dbe8b03b960a560f804ca73b4036a Mon Sep 17 00:00:00 2001 From: mbasadi Date: Thu, 9 Nov 2023 09:06:24 -0500 Subject: [PATCH 2/4] versioning --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b757c3..eb9eb61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "notificationapi-node-server-sdk", - "version": "0.17.2", + "version": "0.18.0", "description": "NotificationAPI server-side library for Node.js", "keywords": [ "notificationapi", From 19706b65bb3861ce598d0e1ecbcc64780e5295d0 Mon Sep 17 00:00:00 2001 From: mbasadi Date: Fri, 10 Nov 2023 15:45:19 -0500 Subject: [PATCH 3/4] User body request does not need id --- src/index.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 98f07bb..254efc6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,20 +41,19 @@ class NotificationAPI { }; /** used to identify your user */ identifyUser = async (user: User): Promise => { + const { id, ...userData } = user; const hashedUserId = `${createHmac('sha256', this.clientSecret as string) - .update(user.id) + .update(id) .digest('base64')}`; const customAuthorization = 'Basic ' + - Buffer.from(`${this.clientId}:${user.id}:${hashedUserId}`).toString( - 'base64' - ); + Buffer.from(`${this.clientId}:${id}:${hashedUserId}`).toString('base64'); return this.request( 'POST', - `users/${encodeURIComponent(user.id)}`, - user, + `users/${encodeURIComponent(id)}`, + userData, customAuthorization ); }; From f48f34121c2345ba5e4b2201c08e8635ca459b0a Mon Sep 17 00:00:00 2001 From: mbasadi Date: Fri, 10 Nov 2023 15:46:02 -0500 Subject: [PATCH 4/4] More complete user example --- src/__tests__/index.test.ts | 39 +++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index f29dadd..cfe9db0 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -7,6 +7,7 @@ import { Channels, CreateSubNotificationRequest, DeleteSubNotificationRequest, + PushProviders, SendRequest, SetUserPreferencesRequest, User @@ -508,15 +509,45 @@ describe('Identify user', () => { const clientId = 'testClientId_identify_user'; const clientSecret = 'testClientSecret_identify_user'; const userId = 'testUserId_identify_user'; - const user = { id: userId }; + const user: User = { + id: userId, + email: 'test+node_server_sdk@notificationapi.com', + number: '+15005550006', + pushTokens: [ + { + type: PushProviders.FCM, + token: 'samplePushToken', + device: { + app_id: 'sample_app_id', + ad_id: 'sample_ad_id', + device_id: 'sample_device_id', + platform: 'sample_platform', + manufacturer: 'sample_manufacturer', + model: 'sample_model' + } + } + ], + webPushTokens: [ + { + sub: { + endpoint: 'sample_endpoint', + keys: { + p256dh: 'sample_p256dh', + auth: 'sample_auth' + } + } + } + ] + }; test('makes API calls with a correct request body', async () => { axiosMock.onPost(userEndPointRegex).reply(200); await notificationapi.init(clientId, clientSecret); - await notificationapi.identifyUser({ id: userId }); + await notificationapi.identifyUser(user); + const { id, ...userData } = user; expect(axiosMock.history.post).toHaveLength(1); - expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(user)); + expect(axiosMock.history.post[0].data).toEqual(JSON.stringify(userData)); expect(axiosMock.history.post[0].url).toEqual( - `https://api.notificationapi.com/${clientId}/users/${userId}` + `https://api.notificationapi.com/${clientId}/users/${id}` ); expect(axiosMock.history.post[0].headers.Authorization).toEqual( `Basic ${Buffer.from(