Meteor. Make sure user is logged into only one tab? - meteor

if people log into my Meteor 1.3.1 app from more than one tab/browser they're going to have problems.
So, when a user logs in, I'd like to log them out everywhere else, and park them on a "you logged in somewhere else" page.
I tried a couple things: Accounts.logoutOtherClients() looked promising, but seems to be broken. This logs everyone out.
if (Meteor.isClient) {
// FF 1 tab, Chrome 2 tabs.
// FF login -> chrome logs out ok.
// Chrome login -> FF logs out. Then Chrome other tab logs in. Then both Chrome tabs logout.
Accounts.onLogin(function() {
console.log(Meteor.loggingIn()); // false. See https://github.com/meteor/meteor/issues/1616
Accounts.logoutOtherClients();
});
}
Before that I was working on a hand-rolled solution, but it seemed like Meteor.logout() would logout both tabs in the same browser. The best I could get was window.location="https://google.com"
NOTE: The approach below doesn't work if user closes all tabs. When they re-open app, they don't have a tabToken, so can never login again.
What's the way to do this?
The best way I can think to do this is:
1) At Meteor.start() on the client make a unique tabToken
2) Save token to Session variable on client
3) At login save tabToken to user
4) On client, listen to Meteor.user().profile for tabToken changes.
if (Meteor.isClient) {
// Make a unique tabToken at startup.
Meteor.startup(function() {
var tabToken = (0|Math.random()*9e6).toString(36); // jshint ignore:line
Session.set('tabToken', tabToken);
console.log('startup TabToken ' + Session.get('tabToken'));
});
// onLogin, set the active tab to our tabToken
Accounts.onLogin(function() {
Meteor.call('setActiveTab', Session.get('tabToken'), function() {
// When method returns, watch profile for tabToken changes
console.log('setActiveTab tabToken ' + Session.get('tabToken'));
console.log('setActiveTab Meteor.user().profile.tabToken ' + Meteor.user().profile.tabToken);
Tracker.autorun(function (c) {
// If the tabToken changes to a different token, logout
if (Meteor.user()) {
var userTabToken = Meteor.user().profile.tabToken;
if (userTabToken && !Session.equals('tabToken', userTabToken)) {
window.location = 'https://google.com';
// Doesn't work
// c.stop();
// Meteor.logout(); // ERROR this logs out all tabs in browser.
// Router.go('loggedOut');
}
}
});
});
});
}
// Make a meteor method to set tabToken
if (Meteor.isServer) {
Meteor.methods({
setActiveTab: function(tabToken) {
if(!tabToken) throw new Meteor.Error('noTabToken');
console.log('setActiveTab ' + tabToken);
if (!Meteor.user()) return;
Meteor.users.update(Meteor.userId(), {
$set: {
'profile.tabToken': tabToken
}
});
}
});
}
This just seems really kludgy. Any other suggestions?
Mike

Related

user object not loaded after logging out and quickly logging back in in Meteor

I am facing a strange behaviour, and can't debug it properly. I need your help.
If I log out of Meteor, everything seems fine.
If I wait for around 2 seconds and log back in again, everything is still very fine. But if I logout and quickly login again, right after the login process, the Meteor.user() object is set to null, which leads my router to redirect the user back to the login page.
Any idea why this is happening, and how could I prevent it?
I have spent 2h trying several things without success. Any suggestion is most welcome.
EDIT
This is my global onBeforeAction function :
Router.onBeforeAction(function() {
// Ensures the user is logged in
if (!Meteor.userId()) {
please_login();
}
// Email address not verified for 24h? Please verify it!
else {
var self = this;
// Waits for the user object to be passed over DDP
function wait_for_user_data() {
if (Meteor.user() && Meteor.user().emails && Meteor.user().profile) {
var user = Meteor.user();
var now = new Date().getTime();
var ca = user.createdAt.getTime();// Created At
var cs = (now - ca) / (24 * 60 * 60 * 1000);// Created Since (in days)
var urls = ["email_verification_required", "email_verification"];
if (cs > 1 &&
!user.emails[0].verified &&
urls.indexOf(self.url.split("/")[1]) == -1) {
Router.go("email_verification_required");
}
else {
self.next();
}
}
else {
setTimeout(wait_for_user_data, 500);
}
}
wait_for_user_data();
}
},
{except: ['home', 'login', 'register', 'password_recovery', "email_verification", "profile"]})
What actually happens is the following :
When I login right after having logged out, self.next() is called, but the current user properties (Meteor.user().emails and Meteor.user().profile) aren't loaded yet for some reason. They are undefined. As you can see, I tried to work around this by waiting until they are defined, but then I receive the following error message :
Route dispatch never rendered. Did you forget to call this.next()
in an onBeforeAction?
This seems to cause Meteor to set Meteor.user() to null, and so my user gets redirected to the login page...
EDIT BIS
This is how I am handling the publish/subscribe of the users data. I have 2 different pub/sub set, one for all users, and the other one for the logged in user only.
Meteor.publish('users', function() {
var args = {};
var fields = {
'_id': 1,
'createdAt': 1,
'profile.firstname' : 1,
'profile.lastname' : 1,
'profile.lang' : 1,
};
// Only admins can access those sensible information
if (this.userId && Roles.userIsInRole(this.userId, 'admin')) {
fields["emails"] = 1;
fields["profile.celular"] = 1;
args : {$ne : {_id: this.userId}};
}
return Meteor.users.find(args, {fields: fields});
});
Meteor.publish('user', function() {
return Meteor.users.find({_id: this.userId});
});
This is how I subscribe to those two publications within my router's configurations :
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
waitOn: function() {
return [Meteor.subscribe('user'), Meteor.subscribe('users')];
},
});
Without a code example (and knowing which router you're using) I'm going to take a guess that you're router code looks something like :
if (!Meteor.user()) {
//... redirect or change tempalte etc
}
There is another value you can check,
if (!Meteor.user() && !Meteor.loggingIn()) {
//... redirect or change tempalte etc
}
you can use various combinations of these to handle the login state, such as having a logging in loading view etc
With more information
The redirect is being called in a callback, currently for the login with password function, having a look at the docs it should be called on the onLogin callback linked as this ensures that login was successful, though doing the redirect here, may not provide the same range of control as doing this in a reactive context such as a before hook on a controller, or an autorun function created in the login template onCreated callback.
The way I've deal with this in the past is to define an explicit subscription to the user object then waitOn that subscription in the router. Much simpler than writing a polling loop but mostly I did it because I needed specific fields that didn't come over by default. Hopefully this can help you with your problem.
Server:
Meteor.publish('me',function(){
if ( this.userId ){
// return the fields I need
return Meteor.users.findOne({ _id: this.userId },{ fields: { field1: 1, field2: 1, ... }});
}
else this.ready();
});
Router:
waitOn: function(){
return Meteor.subscribe('me');
}

Meteor Iron-Router stop running a route

Before a user navigates away from my create post route I would like to ask the user for confirmation. If they say no then stop the new route from running. Currently everything is working expect the new route is always rendering; I can't seem to stop the router.
var isUnsavedCreatePost = function() {
bootbox.confirm("Are you sure?", function(result) {
if (result == true) {
this.next();
} else {
}
});
};
Router.onStop(isUnsavedCreatePost, {only: ['createPost']});

Correct way to manage routes after sign in for different roles

I have 2 roles, admins and users. I have a home route '/' that is just a sign in page. When admin users sign in they must go to one route ('adminPortal') and when user users log in they must go to the 'userPortal' route. On signing out both roles should route back to '/'.
Before I had an admin role, I was routing on sign in like so:
Router.onBeforeAction(function() {
this.render('loading');
if (! Meteor.userId()) {
this.render('Home');
} else {
this.next();
}
});
which worked fine (actually it was breaking my waitOn: render loading template stuff which I just discovered but more on that later). I then added roles like this (from a Stack Overflow answer I can't find right now):
server/
insertUsers=function(){
var adminId=Accounts.createUser({
username:"admin",
password:"password"
});
Roles.addUsersToRoles(adminId,"admin");
var userIdd = Accounts.createUser({
username:"user",
password:"password"
});
Roles.addUsersToRoles(userIdd,"user");
};
and
Meteor.startup(function () {
// always start from scratch (you will want to comment this line at some point !)
Meteor.users.remove({});
if(Meteor.users.find().count()===0){
insertUsers();
}
})
and
Meteor.publish("user", function () {
if (this.userId) {
return Meteor.users.find({_id: this.userId},
{fields: {'roles': 1}});
} else {
this.ready();
}
});
And I tried to route to the user/admin portals like this:
router.js
Router.route('/', {
before: function() {
if (! Meteor.userId()) { // I acutally added this check that the user is not logged in after the infinite loading problem but I thought the question was getting too long so I just left it in rather
this.render('Home')
} else {
if (Roles.userIsInRole(Meteor.user(), 'admin')) {
Router.go('/admin')
} else {
Router.go('/user')
}
}
},
waitOn: function () {
return Meteor.subscribe('user');
}
});
Now this very almost works! If I log is as either user I go to the right portal. However, when I sign out, my onBeforeAction (i.e. first code block in this question) only renders the Home template and does not actually change the URL to '/' (i.e. the URL remains either '/user' or '/admin'). Now when I try log in a second time, it will always take me to the route that I was taken to on the first log in unless I manually change the browser URL to '/'. So I thought I'd just replace the this.render('Home') with a Router.go('/'); but that seems to have created some sort of infinite loop where the Home template never renders (it did incidentally now for the first time correctly render my loading template though).
So thanks for reading all that! What's the right way to do this?
Try adding Router.go('/'); in your logout button event, along with Meteor.logout();
Example:
Template.loginButtons.events({
'click #login-buttons-logout' : function (event, template) {
Meteor.logout(function(err) {
Router.go('/');
});
}
});
I had the same issue as you, and that was the simplest way I've found to return to home page after logout.

How to make sure user in app's session and google's session are the same when using accounts-google

I am using accounts-google on my app and I'd like to solve rather odd authentication scenario.
A logs in so now as an app session and a google session
A switches to gmail and logs out there.
Now, mind you that, A is actually still logged in on the meteor app.
B comes along, logs in to Gmail using his account.
Switches to the meteor app to see that he's logged in, but oddly, logged in with A's account.
This scenario leads to lots of confusions and people unknowingly using other users' accounts where they share computers.
So, basically, I need to users in the meteor session and google session to be the same, and if not, ensure that the current meteor session is invalidated and loginWithGoogle() is called again.
How can I solve this?
It seems impossible with Meteor's current accounts package, although one could create a new one using Google's latest googleplus api.
But there seems to exist a workaround by:
1) Set up onBeforeAction hooks on your router to login the user automatically (which asks for credentials if user is not logged in to external service)
var loginWithGoogle = function() {
if (Meteor.isClient) {
Session.set('loginError', undefined);
Meteor.loginWithGoogle({
loginStyle : "redirect",
requestPermissions : ['profile', 'email'],
requestOfflineToken: true
}, function (err) {
if (err)
Session.set('loginError', 'reason: ' + err.reason + ' message: ' + err.message || 'Unknown error');
});
}
}
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
console.log('The app is automatically asking for you to log in.');
loginWithGoogle();
}
} else {
this.next();
}
}
Router.onBeforeAction(requireLogin, {except: ['some-special-public-route']});
2) Log the user out when they are navigating away from every page (caveat: login/logout gets called everytime the user navigates within the app)
Meteor.startup(function(){
$(window).bind('beforeunload', function() {
closingWindow();
});
});
closingWindow = function(){
console.log('The app is automatically logging you out because you are leaving.');
Meteor.logout();
}
3) improvement area: set a session variable to track user's navigation within the app and run the unload event depending on the variable.

Meteor.user() login issue

I am testing a restrict login function with router code below
var requireLogin = function() {
if (! Meteor.user()) {
console.log("user not logged");
this.next()
} else {
console.log("user logged");
this.next()
}
}
Router.onBeforeAction(requireLogin, {except: ['home','login','about']});
when I try to enter restricted area like userprofile it ask me to log in and print "user not logged"
and after I successfully log in and try to access that area again. it printing both code starts with "user not logged" and then "user logged"
I want to know how to avoid this to happen? since some page become glitched when this happened.
I want it to only print "user logged" if I enter a restricted area page.
Any help would be appreciated.
You need to integrate Meteor.loggingIn() somewhere in your requireLogin function. Because, what's happening is that Meteor is still loading the user system and for every route change, it re-authenticates the user based on current session, if it exists.
var requireLogin = function() {
if(!Meteor.user()){
if(Meteor.loggingIn()){
this.render(this.loadingTemplate);
}else{
this.render('accessDenied');
}
}else {
this.next();
}
}
You will notice that it uses this.loadingTemplate. To keep this, you must also configure your routes to have a loading template. e.g.:
Router.configure({
loadingTemplate: 'loading'
});
or you could just simply swap that out with this.render('loading'); where 'loading' is the template name of your 'Now loading' yield/page.

Resources