A frequent requirement for any app is the need to send different data to different users. Whether it's updates to a social feed, discounts for frequent buyers, a list of matches on a dating platform or any other kind of private or at least user-specific information. Fortunately, all three of deepstreamHub's core concepts - it's realtime datastore, publish-subscribe, and request-response - provide various means to achieve this. The trick? Combine user-specific record or event names with deepstream's permissioning language Valve.
User-Specific Records
Providing private or user-specific records is as simple as including the username in the record name. If your social network has a profile for Lisa Miller, simply store the profile in a record called profile/lisa-miller
:
var profile = ds.record.getRecord( 'profile/lisa-miller' );
Now we need to make sure that everyone can read that profile but only Lisa can edit her information. We can enforce this using Valve. In the record
section of your dashboard's permission file let us create the following rule:
"profile/$username":
read: true
write: "user.id === $username"
How does this rule work? First we specify profile/$username
as a pattern. Whenever a record with a name matching this pattern is accessed, the rule will be applied. read: true
makes sure that everyone can read the record's data. user.id === $username
ensures that the $username
part of the record name needs to match the username the user is currently logged in with if they wish to write.
User-Specific RPCs
Ok, so far, so simple. Let's look at a more advanced example including an HTTP authentication endpoint and a backend process that provides user-specific data as a response to remote procedure calls (RPCs). Say we're running an online pet-food shop and the more frequently a user orders, the higher a discount she gets. This means we need three things in our e-marketplace setup:
- a HTTP-Server that checks the credentials of the user trying to log in,
- a backend process that has access to prices as well as user discounts and can provide a RPC to retrieve a price
- a way to make sure that the username the client provides when asking for the price is in fact their own.
To summarize, our setup will look as follows:
Let's go through the various components step by step, shall we? First off, the client needs to log in. We'll use a very basic login form: username, password, and an button labeled "login" is all we need.
You can find this and all other files for this guide in the accompanying Github repo.
Once the user hits "login", the client executes deepstream's login method, providing the username and password as data.
login() {
this.ds = deepstream( 'wss://123.deepstreamhub.com?apiKey=xxx' ).login({
type: 'webhook',
username: this.username(),
password: this.password()
}, this._onLogin.bind( this ))
}
The username and password now need to be validated. We'll do this by telling deepstreamHub to make an HTTP POST request to a given Webhook-URL. This can be configured in the authentication section of of the dashboard:
We'll process this request with a simple HTTP server written in Express that checks the credentials and returns the HTTP status code 200
for successful and 403
for failed login attempts.
For simplicity's sake we'll use a hardcoded map of cleartext passwords here. In the real world this information would ideally be hashed and stored in a database or provided by an open authentication API.
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
const users = {
'user-a': { password: 'user-a-pass', serverData: { role: 'user' } },
'user-b': { password: 'user-b-pass', serverData: { role: 'user' } },
'data-provider': {
password: 'provider-pass', serverData: { role: 'provider'}
}
}
app.use(bodyParser.json());
app.post('/authenticate-user', function (req, res) {
console.log( 'received auth request for ' + req.body.authData.username );
var user = users[ req.body.authData.username ];
if( user && user.password === req.body.authData.password ) {
res.status( 200 ).json({
username: req.body.authData.username,
serverData: user.serverData
});
} else {
res.sendStatus( 403 );
}
});
app.listen( port, function () {
console.log( `listening on port ${port}` );
});
Did you notice the extra "user" called data-provider
above? We'll use it to authenticate connections from backend processes that can provide data to the user. Such a "provider" would first need to connect and log into deepstreamHub:
const deepstream = require( 'deepstream.io-client-js' );
const ds = deepstream( 'wss://154.deepstreamhub.com?apiKey=xxx' );
const credentials = {
username: 'data-provider',
password: 'provider-pass',
type: 'webhook'
};
ds.login( credentials, ( success, data ) => {
if( success ) {
console.log( 'connected to deepstreamHub' );
}
});
Next up, we'll want to provide
remote procedure call called get-price
. This means we tell deepstream that whenever a client asks for get-price
, this provider will be able to answer that question.
const itemPrice = 100;
const userdata = {
'user-a': { discount: 0.1 },
'user-b': { discount: 0.3 }
}
ds.rpc.provide( 'get-price', ( data, response ) => {
var discount = userdata[ data.username ].discount;
var finalPrice = itemPrice - ( discount * itemPrice );
response.send( finalPrice );
});
Let's look through the snippet above. For simplicity, we specify our price and our various discounts as static data. Now when a user makes a request, they send their username as part of the RPC data:
this.ds.rpc.make( 'get-price', { username: this.username() },
this._onRpcResponse.bind( this ) );
To ensure that the provided username is in fact the one the user is logged in with, we'll use deepstream's permissioning language Valve again:
request: "data.username === user.id"
The rule above means that the provider can be sure to get a valid, authenticated username and can return a price with the correct discount applied.
User-Specific Events
Ok, so what about user-specific "events", deepstream's publish-subscribe mechanism. Fundamentally, they work the same way as records: Make the username a part of the event name and use Valve to ensure that only the right user can subscribe to the right event.