In realtime collaboration, a server will merely act as an online database that triggers events upon data creation/modification/deletion. As long as the above requirement is met, your server can be built in any language and stack of your choice. For the simplicity of this guide, we will be using Firebase.
- Go to the Firebase Console, login and create a project.
- Click "Add Firebase to your Web App" and copy the whole code for "Initializing Firebase". If
storageBucket
is empty, close the popup and try again (that's a known bug from Firebase). - Create a JavaScript file and name it
server.js
. - Paste the code that you have copied from Firebase. (Note that you should remove the script tags)
- Store the firebase.database.References for annotations and users. We will use these to create/update/delete data, and listen to data change events as well.
window.Server = () => {
const config = {
apiKey: "YOUR_API_KEY",
authDomain: "PROJECT_ID.firebaseapp.com",
databaseURL: "https://PROJECT_ID.firebaseio.com",
storageBucket: "PROJECT_ID.appspot.com",
messagingSenderId: "YOUR_SENDER_ID"
};
firebase.initializeApp(config);
this.annotationsRef = firebase.database().ref().child('annotations');
this.authorsRef = firebase.database().ref().child('authors');
};
- Create a custom bind function for authorization and data using firebase.auth.Auth#onAuthStateChanged and firebase.database.Reference#on.
Server.prototype.bind = (action, callbackFunction) => {
switch(action) {
case 'onAuthStateChanged':
firebase.auth().onAuthStateChanged(callbackFunction);
break;
case 'onAnnotationCreated':
this.annotationsRef.on('child_added', callbackFunction);
break;
case 'onAnnotationUpdated':
this.annotationsRef.on('child_changed', callbackFunction);
break;
case 'onAnnotationDeleted':
this.annotationsRef.on('child_removed', callbackFunction);
break;
default:
console.error('The action is not defined.');
break;
}
};
- Define a method to check if author exists in the database. We will use firebase.database.Reference#once and firebase.database.DataSnapshot#hasChild to do so.
Server.prototype.checkAuthor = (authorId, openReturningAuthorPopup, openNewAuthorPopup) => {
this.authorsRef.once('value', (authors) => {
if (authors.hasChild(authorId)) {
this.authorsRef.child(authorId).once('value', (author) => {
openReturningAuthorPopup(author.val().authorName);
});
} else {
openNewAuthorPopup();
}
}.bind(this));
};
- Define a sign-in method. In this guide, we will use firebase.auth.Auth#signInAnonymously.
Server.prototype.signInAnonymously = () => {
firebase.auth().signInAnonymously().catch((error) => {
if (error.code === 'auth/operation-not-allowed') {
alert('You must enable Anonymous auth in the Firebase Console.');
} else {
console.error(error);
}
});
};
From the Firebase console click the "Authentication" button on the left panel and then click the "Sign-in Method" tab, just to the right of "Users". From this page click the "Anonymous" button and choose to enable Anonymous login.
Define data-write methods using firebase.database.Reference#set and firebase.database.Reference#remove.
Server.prototype.createAnnotation = (annotationId, annotationData) => {
this.annotationsRef.child(annotationId).set(annotationData);
};
Server.prototype.updateAnnotation = (annotationId, annotationData) => {
this.annotationsRef.child(annotationId).set(annotationData);
};
Server.prototype.deleteAnnotation = (annotationId) => {
this.annotationsRef.child(annotationId).remove();
};
Server.prototype.updateAuthor = (authorId, authorData) => {
this.authorsRef.child(authorId).set(authorData);
};
Last but not least, you should add server-side permission rules for writing data. Although client-side permission checking is supported in PDF.js Express Web Viewer, every user does have access to each annotation's information (including authorId and authorName). Thus, data-write permission should be regulated in the server as well. In this guide, we have used Firebase's Database Rules.
- Copy the JSON below and paste it in your Firebase Console's Database Rules. From the console click the "Database" button on the left panel and then click the "Rules" tab, just to the right of "Data". This will make sure that trying to modify someone else's annotation isn't allowed.
{
"rules": {
".read": "auth != null",
"annotations": {
"$annotationId": {
".write": "auth.uid === newData.child('authorId').val() || auth.uid === data.child('authorId').val() || auth.uid === newData.child('parentAuthorId').val() || auth.uid === data.child('parentAuthorId').val()"
}
},
"authors": {
"$authorId": {
".write": "auth.uid === $authorId"
}
}
}
}