Skip to content

Instantly share code, notes, and snippets.

@puf
Created January 26, 2016 00:58
Show Gist options
  • Save puf/764d21c973736026497d to your computer and use it in GitHub Desktop.
Save puf/764d21c973736026497d to your computer and use it in GitHub Desktop.
Account merging
var ref = new Firebase('https://yours.firebaseio.com/acknowledge');
var users = [];
function $(selector) {
var result = document.querySelectorAll(selector);
return (result.length > 1) ? Array.prototype.slice.call(result) : result[0];
}
function handleAuthResult(error, authData) {
if (error) {
alert(error);
}
}
// Call this function to indicate the the current user (ref.getAuth().uid) is looking to merge with targetUid
function proposeMerge(targetUid) {
ref.child('merges/proposed').child(ref.getAuth().uid).set(targetUid);
}
// Call this function to indicate the the current user (ref.getAuth().uid) is looking to merge with targetUid
function acceptMerge(targetUid) {
// TODO: should we also delete the proposed merge?
ref.child('merges/accepted').child(targetUid).set(ref.getAuth().uid);
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded');
$('button.signin').forEach(function(button) {
var provider = Array.prototype.slice.call(button.classList).filter(function(claz) { return claz != 'signin' })[0];
button.addEventListener('click', function(e) {
e.preventDefault();
if (!ref.getAuth()) {
if (provider == 'password') {
ref.authWithPassword({ email: prompt('email', '[email protected]'), password: prompt('password', 'firebase') }, handleAuthResult);
}
else {
ref.authWithOAuthPopup(provider, handleAuthResult, { scope: 'email' });
}
}
else {
console.error('You are already authenticated. You should not even be able to click that button.');
}
return false;
});
});
$('button.signout').addEventListener('click', function(e) {
e.preventDefault();
ref.unauth();
return false;
});
ref.onAuth(function(authData) {
// show profile
$('#whoami').textContent = JSON.stringify(authData, null, ' ');
if (authData) {
// save profile data
ref.child("users").child(authData.uid).set(authData[authData.provider]);
// handle any pending merge requests
ref.child('merges/proposed').orderByValue().equalTo(authData.uid).once('value', function(snapshot) {
console.log('You have '+(snapshot.hasChildren() ? snapshot.numChildren() : 'no')+' merge requests');
snapshot.forEach(function(child) {
if (confirm('Do you want to merge with account "'+child.key()+'"')) {
acceptMerge(child.key());
}
});
});
// handle my accepted merge requests
ref.child('merges/accepted').child(authData.uid).once('value', function(snapshot) {
if (snapshot.val()) {
$('#whoami').textContent += '\n You have been merged into account "'+snapshot.val()+'"';
ref.getAuth().merged_uid = snapshot.val();
}
});
}
// show/hide the login/logout buttons
$('#loginBar button').forEach(function(button) {
var show = authData ? 'signout' : 'signin';
button.style.display = button.classList.contains(show) ? '' : 'none';
});
});
// show all users
ref.child('users').on('value', function(snapshot) {
users = snapshot;
$('#users').textContent = '';
snapshot.forEach(function(userSnapshot) {
var li = document.createElement('li');
li.id = 'uid_'+userSnapshot.key().replace(':','');
var pre = document.createElement('pre');
pre.textContent = userSnapshot.key() + ': '+ JSON.stringify(userSnapshot.val(), null, ' ');
li.appendChild(pre);
$('#users').appendChild(li);
});
});
$('#findMe').addEventListener('click', function(e) {
e.preventDefault();
if (ref.getAuth() != null) {
var me = null;
users.forEach(function(userSnapshot) {
if (userSnapshot.key() == ref.getAuth().uid) {
me = userSnapshot;
}
});
ref.child('users').once('value', function(snapshot) {
if (me) {
snapshot.forEach(function(userSnapshot) {
var user = userSnapshot.val();
if (userSnapshot.key() != ref.getAuth().uid && user.email == me.val().email) {
$('#'+'uid_'+userSnapshot.key().replace(':','')).style.backgroundColor = 'yellow';
//if (userSnapshot.val().email == me .val().email) console.log('You might also be: '+userSnapshot.key());
if (confirm('Do you want to propose that your account be merged with the "'+userSnapshot.key()+'" account with mail address '+user.email)) {
proposeMerge(userSnapshot.key());
}
}
})
}
});
}
else {
console.warn('You are not logged in. We cannot match you up with anyone.');
}
return false;
});
$('#addMessage').addEventListener('click', function(e) {
e.preventDefault();
if (ref.getAuth()) {
console.log(ref.getAuth());
ref.child('messages').push({ timestamp: Firebase.ServerValue.TIMESTAMP, uid: ref.getAuth().merged_uid ? ref.getAuth().merged_uid : ref.getAuth().uid })
}
return false;
});
ref.child('messages').limitToLast(3).on('value', function(messages) {
$('#messages').textContent = '';
messages.forEach(function(message) {
var li = document.createElement('li');
li.textContent = JSON.stringify(message.val());
$('#messages').appendChild(li);
});
});
});
var ref = new Firebase('https://yours.firebaseio.com/pairing');
var users = [];
function $(selector) {
var result = document.querySelectorAll(selector);
return (result.length > 1) ? Array.prototype.slice.call(result) : result[0];
}
function handleAuthResult(error, authData) {
if (error) {
alert(error);
}
}
// Call this function to indicate the the current user (ref.getAuth().uid) is looking to merge with targetUid
function proposeMerge(targetUid) {
ref.child('merges/proposed').child(ref.getAuth().uid).set(targetUid);
}
// Call this function to indicate the the current user (ref.getAuth().uid) is looking to merge with targetUid
function acceptMerge(targetUid) {
var updates = {};
updates['merges/proposed/'+targetUid] = null;
updates['merges/accepted/'+targetUid] = ref.getAuth().uid;
ref.update(updates, function(error) {
if (error) {
console.error(error);
alert('Something went wrong while merging accounts: '+error);
}
else {
alert('Your account has been merged into "'+targetUid+'"');
}
});
}
document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded');
$('button.signin').forEach(function(button) {
var provider = Array.prototype.slice.call(button.classList).filter(function(claz) { return claz != 'signin' })[0];
button.addEventListener('click', function(e) {
e.preventDefault();
if (!ref.getAuth()) {
if (provider == 'password') {
ref.authWithPassword({ email: prompt('email', '[email protected]'), password: prompt('password', 'firebase') }, handleAuthResult);
}
else {
ref.authWithOAuthPopup(provider, handleAuthResult, { scope: 'email' });
}
}
else {
console.error('You are already authenticated. You should not even be able to click that button.');
}
return false;
});
});
$('button.signout').addEventListener('click', function(e) {
e.preventDefault();
ref.unauth();
return false;
});
ref.onAuth(function(authData) {
// show profile
$('#whoami').textContent = JSON.stringify(authData, null, ' ');
if (authData) {
// save profile data
ref.child("users").child(authData.uid).set(authData[authData.provider]);
// handle any pending merge requests
ref.child('merges/proposed').orderByValue().equalTo(authData.uid).once('value', function(snapshot) {
console.log('You have '+(snapshot.hasChildren() ? snapshot.numChildren() : 'no')+' merge requests');
snapshot.forEach(function(child) {
if (confirm('Do you want to merge with account "'+child.key()+'"')) {
acceptMerge(child.key());
}
});
});
// handle my accepted merge requests
ref.child('merges/accepted').child(authData.uid).once('value', function(snapshot) {
if (snapshot.val()) {
$('#whoami').textContent += '\n You have been merged into account "'+snapshot.val()+'"';
ref.getAuth().merged_uid = snapshot.val();
}
});
}
// show/hide the login/logout buttons
$('#loginBar button').forEach(function(button) {
var show = authData ? 'signout' : 'signin';
button.style.display = button.classList.contains(show) ? '' : 'none';
});
});
// show all users
ref.child('users').on('value', function(snapshot) {
users = snapshot;
$('#users').textContent = '';
snapshot.forEach(function(userSnapshot) {
var li = document.createElement('li');
li.id = 'uid_'+userSnapshot.key().replace(':','');
var pre = document.createElement('pre');
pre.textContent = userSnapshot.key() + ': '+ JSON.stringify(userSnapshot.val(), null, ' ');
li.appendChild(pre);
$('#users').appendChild(li);
});
});
$('#pair').addEventListener('click', function(e) {
e.preventDefault();
if (ref.getAuth() != null) {
var code = prompt('Enter the code you received or press enter to generate a new code.');
if (code) {
// if the
ref.child('pairingCodes').child(code).once('value', function(snapshot) {
var otherUid = snapshot.val().uid;
if (otherUid) {
proposeMerge(otherUid);
snapshot.ref().remove(); // remove the pairing code
alert('Proposed to merge with "'+otherUid+'". Now log in with the other account and accept the merge request.');
}
else {
alert('No such code exists. Try again.');
}
}, function(error) {
alert('Error reading pairing code: '+error);
});
}
else {
code = Math.trunc(100000 + Math.random()*899999);
// TODO: should we use a transaction to ensure the code is unique? Or verify that in security rules?
ref.child('pairingCodes').child(code).set({ timestamp: Firebase.ServerValue.TIMESTAMP, uid: ref.getAuth().uid }, function(error) {
if (!error) {
alert('Your code is '+code+'\n. Log in with your other account and enter this code to pair the accounts.')
}
else {
console.error(error);
alert('Something went wrong while generating your pairing code. Please try again...');
}
});
}
}
else {
console.warn('You are not logged in. We cannot match you up with anyone.');
}
return false;
});
$('#addMessage').addEventListener('click', function(e) {
e.preventDefault();
if (ref.getAuth()) {
console.log(ref.getAuth());
ref.child('messages').push({ timestamp: Firebase.ServerValue.TIMESTAMP, uid: ref.getAuth().merged_uid ? ref.getAuth().merged_uid : ref.getAuth().uid })
}
return false;
});
ref.child('messages').limitToLast(3).on('value', function(messages) {
$('#messages').textContent = '';
messages.forEach(function(message) {
var li = document.createElement('li');
li.textContent = JSON.stringify(message.val());
$('#messages').appendChild(li);
});
});
});
{
// This rules file contains two merging strategies:
// 1. acknowledge - requires user A to propose a merge and user B to accept it. Once accepted, the user A
// can only write messages which have user B as the uid.
// 2. pairings - User A generates a secret (that is written into the database) that they send to user B,
// user B enters the secret, which then starts the merge process of approach #1
"rules": {
"acknowledge": {
".read": true,
"users": {
"$uid": {
".write": "auth.uid == $uid"
}
},
"merges": {
// there are two lists here: proposed and accepted merges. Both are a mapping from uid of users A to uid of user B in the first explanation
"proposed": {
".indexOn": [".value"],
"$uid": {
// a merge can only be proposed by the current user
".write": "$uid == auth.uid"
}
},
"accepted": {
"$uid": {
// a merge can only be accepted by the user who is targetted in the merge and only when the merge has been proposed
".write": "auth.uid == newData.val() && newData.parent().parent().child('proposed').child($uid).val() == newData.val() "
}
}
},
"messages": {
// unmerged users can write messages under their own uid. Merged users can write messages under their merge target's uid.
"$messageid": {
".write": "(!newData.parent().parent().child('merges/accepted').child(auth.uid).exists() && auth.uid === newData.child('uid').val()) ||
( newData.parent().parent().child('merges/accepted').child(auth.uid).val() == newData.child('uid').val() )
"
}
}
},
"pairing": {
"users": {
".read": true,
"$uid": {
".write": "auth.uid == $uid"
}
},
"pairingCodes": {
// this node contains a list of pairing codes. The list itself is not readable, which prevents people from looking up all pairingCodes
"$code": {
// a pairing code consists of a uid and a timestamp. The uid identifies the user that generated the code.
".validate": "newData.hasChildren(['uid', 'timestamp'])",
// you can read a specific pairingCode if you know it and it's less than a minute old
".read": "data.child('timestamp').val() + 60000 > now",
// you can only write data if nothing exists yet, or delete existing data
".write": "(newData.exists() && !data.exists()) || (data.exists() && !newData.exists())",
"timestamp": {
".validate": "newData.val() == now"
}
}
},
"merges": {
// the rules for the merges are the same as in the first approach
".read": true,
"proposed": {
".indexOn": [".value"],
"$uid": {
".write": "$uid == auth.uid || newData.parent().parent().child('accepted').child($uid).val() == auth.uid"
}
},
"accepted": {
"$uid": {
".write": "auth.uid == newData.val() &&
data.parent().parent().child('proposed').child($uid).val() == newData.val() &&
!newData.parent().parent().child('proposed').child($uid).exists()
"
}
}
},
"messages": {
".read": true,
"$messageid": {
".write": "(!newData.parent().parent().child('merges/accepted').child(auth.uid).exists() && auth.uid === newData.child('uid').val()) ||
( newData.parent().parent().child('merges/accepted').child(auth.uid).val() == newData.child('uid').val() )
"
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment