Skip to content

Commit

Permalink
DEVEXP-562: Add templates/server for GA APIs (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
asein-sinch authored Sep 24, 2024
1 parent 5169b10 commit 7e4ba94
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 0 deletions.
23 changes: 23 additions & 0 deletions templates/server/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Express related configuration
port = 3001

# The secret value used for webhook calls validation
# See https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/
NUMBERS_WEBHOOK_SECRET = <Your webhook secret configured for the Numbers API>

# Application related credentials, used by:
# - Verification
# - Voice
SINCH_APPLICATION_KEY = <Your Application Key>
SINCH_APPLICATION_SECRET = <Your Application Secret>

# Unified related credentials, used by the webhook business logic to send SMS batches.
# US/EU are the only supported regions with unified credentials
SINCH_PROJECT_ID = <Your Sinch Project ID>
SINCH_KEY_ID = <Your Sinch Key ID>
SINCH_KEY_SECRET = <Your Sinch Key Secret>
SMS_REGION = us
# SMS Service Plan ID related credentials.
# If set, these credentials will be used and enable to use regions different of US/EU (e.g.: 'au', 'br' or 'ca').
#SINCH_SERVICE_PLAN_ID = <Your Service Plan ID>
#SINCH_API_TOKEN = <Your Service Plan Token>
79 changes: 79 additions & 0 deletions templates/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Backend application built using Sinch Node.js SDK to handle incoming webhooks

This directory contains a server application based on the [Sinch Node.js SDK](https://github.com/sinch/sinch-sdk-node).

## Requirements

- [Node.js LTS](https://nodejs.org/en)
- [Express](https://expressjs.com/)
- [Sinch account](https://dashboard.sinch.com/)
- [ngrok](https://ngrok.com/docs)

## Configuration
Edit the [.env](.env) file to set the parameters that will be used to configure the Express server and the controllers.

### Server port
*Default: 3001*
- `port`: the port to be used to listen to incoming requests. Default is `3001` if not set.

### Controller Configuration
- Numbers controller: `NUMBERS_WEBHOOK_SECRET`. This value can be found thanks to the [Numbers API](https://developers.sinch.com/docs/numbers/api-reference/numbers/tag/Numbers-Callbacks/), using the `/callbackConfiguration` endpoint.
- SMS controller: no support for header validation at the moment
- Verification and Voice controllers: the incoming `Authorization` header is signed with the same credentials as when using the API:
- `SINCH_APPLICATION_KEY`=Your Application Key
- `SINCH_APPLICATION_SECRET`=Your Application Secret

## Usage

### Starting the server

1. Install the dependencies by running the command `npm install`.
2. Edit the `.env` file with your own parameters (see the paragraph above for details).
3. Run the code with one of the following commands:
- `npm start`
- `node src/server.js`

### Endpoints

When the server is up and running, the declared controllers will listen and respond to the following endpoints:

| Service | Endpoint |
|--------------|--------------------|
| Numbers | /NumbersEvent |
| SMS | /SmsEvent |
| Verification | /VerificationEvent |
| Voice | /VoiceEvent |

## Use ngrok to forward the event request to the local server

You can forward the request to your local server of the port it is listening to.

*Note: The `3001` value is coming from the default configuration and can be changed (see [Server port](#Server port) configuration section)*

```bash
ngrok http 3001
```

The `ngrok` output will contain something like:
```
ngrok (Ctrl+C to quit)
...
Forwarding https://cafe-64-220-29-200.ngrok-free.app -> http://localhost:3001
```
The line
```
Forwarding https://cafe-64-220-29-200.ngrok-free.app -> http://localhost:3001
```
contains the "`https://cafe-64-220-29-200.ngrok-free.app`" value which will be used to determine the callback URLs, e.g.:
- Numbers: https://cafe-64-220-29-200.ngrok-free.app/NumbersEvent
- SMS: https://cafe-64-220-29-200.ngrok-free.app/SmsEvent
- Verification: https://cafe-64-220-29-200.ngrok-free.app/VerificationEvent
- Voice: https://cafe-64-220-29-200.ngrok-free.app/VoiceEvent

This value must be used to configure the callback URLs:
- Numbers: the callback URL must be set at Number level with the parameter `callbackUrl` when renting a number or when updating a rented number with the API.
- SMS: the callback URL must be configured at Service level on the [Sinch dashboard](https://dashboard.sinch.com/sms/api/services) in the "Callback URLs" section.
- Verification: the callback URL must be configured at Application level on the [Sinch dashboard](https://dashboard.sinch.com/verification/apps) in the "Settings/Callback URL" section.
- Voice: the callback URL must be configured at Application level on the [Sinch dashboard](https://dashboard.sinch.com/voice/apps) in the "Settings/Handle call events with" section.


16 changes: 16 additions & 0 deletions templates/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@sinch/sdk-client-quickstart-template-server",
"version": "0.0.0",
"author": "Sinch",
"description": "",
"type": "module",
"scripts": {
"start": "node src/server.js",
"compile": "tsc"
},
"dependencies": {
"@sinch/sdk-core": "^1.1.0",
"dotenv": "^16.4.5",
"express": "^4.20.0"
}
}
30 changes: 30 additions & 0 deletions templates/server/src/numbers/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NumbersCallbackWebhooks } from '@sinch/sdk-core';
import { handleNumbersEvent } from './serverBusinessLogic.js';

export const numbersController = (app) => {

const callbackSecret = process.env.NUMBERS_WEBHOOK_SECRET || '';
const numbersCallbackWebhooks = new NumbersCallbackWebhooks(callbackSecret);

app.post('/NumbersEvent', async (req, res) => {

// Ensure the authentication is valid before processing the request
const validAuth = numbersCallbackWebhooks.validateAuthenticationHeader(
req.headers,
req.rawBody,
);

// Authentication header failed
if (!validAuth) {
return res.status(401).json();
}

// Parse the request payload
const event = numbersCallbackWebhooks.parseEvent(req.body);

// Let the business layer process the request
await handleNumbersEvent(event);

return res.status(200).json();
});
};
11 changes: 11 additions & 0 deletions templates/server/src/numbers/serverBusinessLogic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @typedef {import("@sinch/sdk-core").NumbersCallback} NumbersCallback
*/

/**
* Handles a Numbers Event.
* @param {NumbersCallback} numbersEvent - The incoming Numbers event notification.
*/
export const handleNumbersEvent = async (numbersEvent) => {
console.log(`Handling Numbers event:\n${JSON.stringify(numbersEvent, null, 2)}`);
};
38 changes: 38 additions & 0 deletions templates/server/src/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import express from 'express';
import { numbersController } from './numbers/controller.js';
import { smsController } from './sms/controller.js';
import { verificationController } from './verification/controller.js';
import { voiceController } from './voice/controller.js';
import * as dotenv from 'dotenv';
dotenv.config();

const app = express();
const port = process.env.port || 3001;

// Middleware to capture the raw body and store it in req.rawBody
app.use((req, res, next) => {
let data = '';

req.on('data', (chunk) => {
data += chunk;
});

req.on('end', () => {
req.rawBody = data;
req.body = JSON.parse(data);
next();
});

req.on('error', (err) => {
next(err);
});
});

numbersController(app);
smsController(app);
verificationController(app);
voiceController(app);

app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
52 changes: 52 additions & 0 deletions templates/server/src/sms/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { SmsCallbackWebhooks } from '@sinch/sdk-core';
import {
handleIncomingBinaryMessageEvent,
// TODO: uncomment with the version 1.2.0
// handleIncomingMmsEvent,
handleIncomingSmsEvent,
handleMmsDeliveryReportEvent,
handleMmsRecipientDeliveryReportEvent,
handleSmsDeliveryReportEvent,
handleSmsRecipientDeliveryReportEvent,
} from './serverBusinessLogic.js';

export const smsController = (app) => {

const smsCallbackWebhooks = new SmsCallbackWebhooks();

app.post('/SmsEvent', async (req, res) => {

// Parse the request payload
const event = smsCallbackWebhooks.parseEvent(req.body);

// Let the business layer process the request
switch (event.type) {
case 'mo_text':
handleIncomingSmsEvent(event);
break;
case 'mo_binary':
handleIncomingBinaryMessageEvent(event);
break;
// TODO: uncomment with the version 1.2.0
// case 'mo_media':
// handleIncomingMmsEvent(event);
// break;
case 'delivery_report_sms':
handleSmsDeliveryReportEvent(event);
break;
case 'delivery_report_mms':
handleMmsDeliveryReportEvent(event);
break;
case 'recipient_delivery_report_sms':
handleSmsRecipientDeliveryReportEvent(event);
break;
case 'recipient_delivery_report_mms':
handleMmsRecipientDeliveryReportEvent(event);
break;
default:
res.status(500).json({ error: 'Unsupported event type' });
}

return res.status(200).json();
});
};
59 changes: 59 additions & 0 deletions templates/server/src/sms/serverBusinessLogic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// eslint-disable-next-line no-unused-vars
import { Sms } from '@sinch/sdk-core';

/**
* Handles an incoming SMS Event.
* @param {Sms.MOText} incomingSmsEvent - The incoming SMS event.
*/
export const handleIncomingSmsEvent = (incomingSmsEvent) => {
console.log(`Handling incoming SMS event:\n${JSON.stringify(incomingSmsEvent, null, 2)}`);
};

/**
* Handles an incoming binary message Event.
* @param {Sms.MOBinary} incomingBinaryMessageEvent - The incoming binary message event.
*/
export const handleIncomingBinaryMessageEvent = (incomingBinaryMessageEvent) => {
console.log(`Handling incoming binary message event:\n${JSON.stringify(incomingBinaryMessageEvent, null, 2)}`);
};

// TODO: uncomment with the version 1.2.0
// /**
// * Handles an incoming MMS Event.
// * @param {Sms.MOMedia} incomingMmsEvent - The incoming MMS event.
// */
// export const handleIncomingMmsEvent = (incomingMmsEvent) => {
// console.log(`Handling incoming MMS event:\n${JSON.stringify(incomingMmsEvent, null, 2)}`);
// };

/**
* Handles an SMS Delivery Report Event.
* @param {Sms.DeliveryReport} smsDeliveryReportEvent - The SMS delivery report event.
*/
export const handleSmsDeliveryReportEvent = (smsDeliveryReportEvent) => {
console.log(`Handling SMS delivery report event:\n${JSON.stringify(smsDeliveryReportEvent, null, 2)}`);
};

/**
* Handles an MMS Delivery Report Event.
* @param {Sms.DeliveryReport} mmsDeliveryReportEvent - The MMS delivery report event.
*/
export const handleMmsDeliveryReportEvent = (mmsDeliveryReportEvent) => {
console.log(`Handling MMS delivery report event:\n${JSON.stringify(mmsDeliveryReportEvent, null, 2)}`);
};

/**
* Handles an SMS Recipient Delivery Report Event.
* @param {Sms.RecipientDeliveryReport} smsRecipientDeliveryReportEvent - The SMS recipient delivery report event.
*/
export const handleSmsRecipientDeliveryReportEvent = (smsRecipientDeliveryReportEvent) => {
console.log(`Handling SMS recipient delivery report event:\n${JSON.stringify(smsRecipientDeliveryReportEvent, null, 2)}`);
};

/**
* Handles an MMS Recipient Delivery Report Event.
* @param {Sms.RecipientDeliveryReport} mmsRecipientDeliveryReportEvent - The MMS recipient delivery report event.
*/
export const handleMmsRecipientDeliveryReportEvent = (mmsRecipientDeliveryReportEvent) => {
console.log(`Handling MMS recipient delivery report event:\n${JSON.stringify(mmsRecipientDeliveryReportEvent, null, 2)}`);
};
49 changes: 49 additions & 0 deletions templates/server/src/verification/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { VerificationCallbackWebhooks } from '@sinch/sdk-core';
import { handleVerificationRequestEvent, handleVerificationResultEvent } from './serverBusinessLogic.js';

export const verificationController = (app) => {

const applicationKey = process.env.SINCH_APPLICATION_KEY;
const applicationSecret = process.env.SINCH_APPLICATION_SECRET;

const verificationCallbackWebhooks = new VerificationCallbackWebhooks({
applicationKey,
applicationSecret,
});

app.post('/VerificationEvent', async (req, res) => {

// Ensure the authentication is valid before processing the request
const validAuth = verificationCallbackWebhooks.validateAuthenticationHeader(
req.headers,
req.rawBody,
'/VerificationEvent',
'POST',
);

// Authentication header failed
if (!validAuth) {
return res.status(401).json();
}

// Parse the request payload
const event = verificationCallbackWebhooks.parseEvent(req.body);

// Let the business layer process the request
let response;
switch (event.event) {
case 'VerificationRequestEvent':
response = handleVerificationRequestEvent(event);
break;
case 'VerificationResultEvent':
response = handleVerificationResultEvent(event);
break;
default:
res.status(500).json({ error: 'Unsupported event type' });
}

console.log(`JSON response:\n${JSON.stringify(response, null, 2)}`);

return res.status(200).json(response);
});
};
24 changes: 24 additions & 0 deletions templates/server/src/verification/serverBusinessLogic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// eslint-disable-next-line no-unused-vars
import { Verification } from '@sinch/sdk-core';

/**
* Handles a Verification Request Event.
* @param {Verification.VerificationRequestEvent} verificationRequestEvent - The incoming Verification Request event.
* @return {Verification.VerificationRequestEventResponse} the verification request event response
*/
export const handleVerificationRequestEvent = (verificationRequestEvent) => {
console.log(`Handling Verification Request event:\n${JSON.stringify(verificationRequestEvent, null, 2)}`);

// add your logic here according to SMS, FlashCall, PhoneCall, ... verification
return {};
};

/**
* Handles a Verification Result Event.
* @param {Verification.VerificationResultEvent} verificationResultEvent - The incoming Verification Result notification.
* @return {null}
*/
export const handleVerificationResultEvent = (verificationResultEvent) => {
console.log(`Handling Verification Result event:\n${JSON.stringify(verificationResultEvent, null, 2)}`);
return null;
};
Loading

0 comments on commit 7e4ba94

Please sign in to comment.