Re-engage offline users with notification fallbacks Part 2: Subscribe to private channels through authentication endpoints
Familiarity with HTML and JavaScript would be beneficial, but aren’t compulsory to complete this tutorial as this is a step by step guide.
This tutorial series explores how the new Pusher Channels publish with info API extension makes it easy to determine when a user is offline, and then fallback to notifications via Beams so that they are kept up to date and you can bring them back to your app.
If you have missed the first part of this tutorial series and would like to start from the beginning, please refer to Part 1. Setting up your environment and building authentication with GitHub SSO.
Adding Channels
The next thing we are going to do is add a Channels capability to our app. By the end of this section you will have an authentication endpoint running on your server that will allow your app to subscribe to a private-channel.
You will then be able to trigger notifications to this channel from the Pusher debug console and this will update a table with a new notifications.
Set up an authentication endpoint on your server
Initialise Channels server SDK
In your server.js file the first thing to do is initialise the Channels server SDK. We already installed the dependency as part of our app setup so we just need to add the following line under our import section (If you do need to install the Channels server SDK in future you can do so by running npm install pusher
):
//Imports
const ChannelsNotifications = require('pusher');
Setting up a new Channels client
We then need to setup a new Channels client. The Channels client requires an appId, key, secret and cluster which we set in our .env file earlier. We will also add a variable for the relative path to the channels auth endpoint which we will use shortly. Add the following config under //Channels config
and then declare a new channelsClient
:
//Channels config
let appId = process.env.APP_ID
let key = process.env.APP_KEY
let secret = process.env.CHANNELS_SECRET_KEY
let cluster = process.env.CLUSTER
let channelsauthEndpoint='/pusher/channels-auth'
const channelsclientConfig = {
appId,
key,
secret,
cluster,
}
const channelsClient = new ChannelsNotifications(channelsclientConfig);
In our client app we will associate a user to a Channels channel with the following naming convention private-userchannel-<userId>
. This means we need an authentication endpoint that can do the following:
- Check that when a user makes an authentication request that the user has a valid authentication token. We can use the
ensureAuthenticated
function we implemented earlier to check that the GitHub token that will be passed in as part of the request to this endpoint is valid. - We also need to ensure that if the channel name starts with
private-userchannel
that the userId suffix in the channel name matches the userId that corresponds with the session token. This checks that not only does the user have a valid token but they are also authorised to join the requested channel based on theiruserid
. If we committed this check any user with a valid session token could join any user channel. - Channels authentication requests are made using a post request, so our channels authentication endpoint should expect a post request.
Taking all these requirements into account add the following under Channels Auth
:
//Channels Auth
app.post('/pusher/channels-auth', ensureAuthenticated, function(req, res) {
// Do your normal auth checks here. Return forbidden if session token is invalid 🔒
const userId = req.user.username; // Get user id from auth system based on session token
const channelName = req.body.channel_name;
const socketId = req.body.socket_id;
var isUserChannel=false;
if (channelName.startsWith('private-userchannel')){
isUserChannel=true;
}
if (isUserChannel && channelName !== 'private-userchannel-'+userId) {
res.status(401).send('Inconsistent request'); //If userid does not m
} else {
const channelsToken = channelsClient.authenticate(socketId, channelName);
res.send(channelsToken);
}
});
The first thing the auth endpoint does is check the session token is valid with the ensureAuthenticated
step. The userId
is determined by the user profile returned from this step and accessible through req.user.username
. We then check if the channel name is a userchannel
and if it is if the channel name doesn’t match the format of private-userchannel-userid
we reject the request.
The final thing we need to do is make some of the channels config available to our client app, and we can do this by passing this to our render function from earlier. Update it as follows:
//Render
app.get('/', ensureAuthenticated, function(req, res){
res.render('index', {
user: req.user.username,
key,
cluster,
channelsauthEndpoint,
})
});
You can now restart the server.
Add Channels to the client app
Returning to our index.hbs file the first thing we will do is import pusher-js into our app. Under the <!--Imports-->
section add:
<!--Imports-->
<script src="https://js.pusher.com/7.0.3/pusher.min.js"></script>
We will then add a simple html table. We will add a row to the table each time we get a new channels message received on our private-userchannel
The first thing to do is to add the following to the html body under <!-- Channels table-->
:
<!--Channels table-->
<table id="channelsnotifications">
<tr>
<th>Title</th>
<th>Message</th>
</tr>
</table>
Currently this will just display a heading row on our homepage. The next thing to do is add the following JavaScript function that we can call to add a row to the table. We will call this function each time we get a new Channels message shortly.
For now the function will find the table by the html id, insert a row after the heading (so newest messages will always be the top row) and insert a cell for the notification title and notification text. Additionally we will style the row with a red indicator if the message is tagged as high priority. We will use this information in our fallback mechanism later.
// Notification table
function tableUpdate(data){
var table = document.getElementById('channelsnotifications');
var row = table.insertRow(1);
var cell1 = row.insertCell(0);
if (data.highPriority === true) {
cell1.id='highPriority';
}else{
cell1.id='lowPriority';
}
var cell2 = row.insertCell(1);
cell2.id='noPriority';
cell1.innerHTML = data.notificationTitle;
cell2.innerHTML = data.notificationText;
}
Initialising a Channels client
To initialise channels in our JavaScript we need to firstly set our config variables; by using the config passed in to our template by the render function we modified above:
// Constants
const userId="{{ user }}"
const appKey = "{{ key }}"
const cluster = "{{ cluster }}"
const channelsauthEndpoint = "{{ channelsauthEndpoint }}"
We can then initialise a new Channels client. This will initiate a new websocket connection to the cluster where our app is located.
// Channels Initialisation
const pusher = new Pusher(appKey, {
cluster: cluster,
authEndpoint: channelsauthEndpoint
});
Under this we need to subscribe to a channel and then bind to messages that contain the event name “notification”:
//Subscribe channel and bind to event
const channel = pusher.subscribe('private-userchannel-'+userId);
channel.bind('notification', function(data) {
tableUpdate(data);
});
What happens here is that calling pusher.subscribe will start subscription request to your userid channel. Because the channel is prefixed with ‘private’ this will automatically perform an authentication callback to your authentication endpoint that we setup earlier. If the authentication request is successful the client will pass an authentication token to the Channels API and the Channels API will complete the subscription request. Any messages then sent to the channel will be delivered to the client.
The channel.bind step is important, because if the client doesn’t explicitly bind to messages by the event name the messages will just be unprocessed. By binding to the ‘notification’ event name any messages with that event name will be processed. In this case for each message received the tableUpdate function will be called and the message will be added as a new row in our table.
Trigger a test notification on the debug console
Finally to test that this is all working as expected, open up a web browser and navigate to your index page. In another tab open up the Channels debug console, by logging into the Channels dashboard, finding your channels app and selecting the the Debug console. Then send a notification as follows:
Channel: private-userchannel-<userid>
userid
should be your GitHub username and should be the id shown on the logout button created earlier
Event: notification
Data:
{
"notificationTitle":"Hello world!",
"notificationText": "You have a new message",
"highPriority": true
}
This should look as follows:
Finally hit Send event and return to the index page. You should see the following.
Congratulations you are now sending realtime messages!
If you send another message with highPriority: false what happens?
Continue to the final part of this tutorial series to learn how to receive web push notifications by associating your GitHub User ID using Beams authenticated users, and re-engage offline users.
11 June 2021
by Pusher team, Chris Casey