How to remove user id from server when the user logs out?


#1

Okay, the title is horrible but I couldn’t find a way to shorten my problem. So I hope I can explain it here:

I want to publish some information filtered by user information. For example:

Lets say there is a Fruits collection which contains 3 objects:

{name: "Apple", color: "Red"}
{name: "Orange", color: "Orange"}
{name: "Banana", color: "Yellow"}

There is another collection, a user data collection, which contains 1 object:

{userid: "123qdasc23d", color: ["Red", "Orange"]}

I want to publish Fruit collection but only user specific color fruits. So Meteor should give only “Apple” and “Orange” fruits, not “Banana”. And whenever user logs out, they should disappear.


#2

Use this.userId:

Meteor.publish('fruits', function () {
  if (this.userId) {
    // Publish fruits for this user if logged in
    return Fruits.find({color: {$in: Meteor.user().color}}); // Meteor.user().color is an **array** in this case
  }
  // Otherwise don't publish data
  return this.ready();
});

Safer version (only for public data):

// server
Meteor.publish('user-data', function () {
  if (! this.userId) {
    this.ready();
  }
  return UserData.find({userId: this.userId});
});

Meteor.publish('fruits', function (userId) {
  var colors = UserData.findOne({userId: userId}).color;

  return Fruits.find({color: {$in: colors}});
});

// client
Tracker.autorun(function () {
  var userId = UserData.findOne({userId: Meteor.userId()}).color
  Meteor.subscribe(userId);
});

#3

The canonical way to reactively update the published items (including removal on logout) is to use publish/subscribe. The Meteor Guide covers this very thoroughly.

You will also find the Security section on publications to be useful.

Alternatively, you can easily hide a presented list of items when a user logs out by using the currentUser helper if you’re using Blaze.


#4

Note that publications are not reactive, so your example will not work as you expect (the if clause will only be evaluated once) unless you either reactively force a re-subscribe from the client, or remove the if and construct the query to include this.userId as part of the find.


#5

This worked really well, thanks a lot!

You wrote return null first, but then you changed it to return this.ready(). Why is that?

Also, if I want to return multiple users’ color preferences combined, how should I modify the publishing function?


#6

True, but they are re-run whenever the logged-in user changes, and we’re not concerned with a possibly-changing userId field on the documents we want to find in this case.

I agree that the alternative is safer though, I’ve updated it thanks!


#7

But the safer version doesn’t deliver the data automatically when logged in. I have to refresh the page.

PS: Log out removes the data automatically.


#8

is userData your collection name for user data?


#9

No, it’s not userData. Why?


#10

Because if you just copy/pasted @reoh’s code that’s what you’ve got.


#11

Also, your original code used userid, whereas the code as shown uses userId.


#12

I think return null is for autopublish, for named pubs this.ready() tells the subscriber not to wait for data.

You are right about the lack of reactivity on the ‘safe version’ - for that you can either publish the UserData document to the client and pass that in the Meteor.subcribe param or use the low-level API

Multi colors:

Meteor.publish(function (userIds /* send an array of ids from client */) {
  // If you also want the currently logged in user
  userIds.push(this.userId);

  var colors = [];

  UserData.find({userId: {$in: userIds}}).forEach(function (data) {
    colors.push(data.color);
  });
  
  // use underscore to remove duplicates
  colors = _.unique(colors);

  return Fruits.publish({color: {$in: colors}});
});

#13

Meteor finds the data. The problem is, I have to refresh the page to get the code working. “Unsafe” version of the code was dependent on user login so Meteor was able to publish or not publish the data reactively. The safer version doesn’t have this capability.


#14

There is nothing preventing “safe” publications from reactively changing the results based on userId - that’s their intended purpose. I think there have been a number of crossed wires here and I’m probably guilty of crossing most of them!

From @reoh’s earlier safe code, is the issue here:

var colors = UserData.find({userId: this.userId}).color;

find returns a cursor, so you can’t select a property (color) on it. @reoh solves this in the last example by using forEach to iterate over the documents, but a solution might be to use findOne rather than find, which returns a (single) document.


#15

Here’s a low-level version suitable for private data:

// psuedo-code
UserData = new Mongo.Collection('user-data');
Fruits = new Mongo.Collection('fruits');

Meteor.publish('fruits', function () {
  var self = this;
  var colors = [];
  
  var handle = UserData.find({userId: this.userId}).observeChanges({
    // Runs whenever a document is added to user data collection
    // and on first run when it initialises
    added: function (id, fields) {
      // store current colors for later use
      colors = fields.color
      Fruits.find({color: {$in: fields.color}}).forEach(function (fruit) {
        // publishes fruits with colors matching the user data
        self.added('fruits', fruit._id, fruit);
      });
    },
    // Runs whenever a document in the user data collection is edited
    changed: function (id, fields) {
      // If 'color' field on user data doc was updated
      if (fields.color) {
        // publish new fruits with matching colors
        Fruits.find({color: {$in: fields.color}}).forEach(function (fruit) {
          self.added('fruits', id, fields)
        });

        // clean up old fruits we don't want anymore on client
        Fruits.find({color: {$in: colors}}).forEach(function (fruit) {
          self.removed('fruits', fruit._id);
        });

        // store updated colors
        colors = fields.color;
      }
    },
    // and removed
    removed: function (id) {
      Fruits.find({color: {$in: colors}}).forEach(function (fruit) {
        // clean up
        self.removed('fruits', fruid._id);
      });
    }
  });

  self.ready();

  self.onStop(function () {
    handle.stop();
  });
});

#16

Thanks for the code! But I don’t understand why I need something this complex? Why do I have to use observeChanges ? Shouldn’t Meteor take care of this automatically?


#17

Usually, it does - but it depends on changes to the parameter you pass to the subscribe function to re-run the publication in full.

Changes to cursors don’t re-run the publication function though (because it uses the added, changed, removed for updates), so you have move that code into the actual callbacks that the publication is using behind the scenes - this will make sure that the fruits data stays up-to-date with UserData.