Created
January 26, 2016 00:58
-
-
Save puf/764d21c973736026497d to your computer and use it in GitHub Desktop.
Account merging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
// 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