DDP.connect + subscription works, but no documents

Sorry if this has been covered in many disparate places, but I think I’ve crawled the entire internet and still can’t figure out the last part of creating a second Meteor app to connect to the first app’s db.

Scenario: there is a consumer-facing app “App A”, and now I’m creating an Admin app “App B” that connects to app A. Let me share code to illustrate the issue (please note I’m omitting some code that handles performance/paging/security so that only the relevant code is shown):

App A lives on localhost:3000 and is started up with: meteor
App B lives on localhost:4000 and is started up with: MONGO_URL="mongodb://127.0.0.1:3001/meteor" meteor --port 4000

// App A: /imports/api/server/pubs.js
Meteor.publish('matrixData', function () {
    var user = Meteor.users.findOne({_id: this.userId});
    console.info('matrixData', user._id, user.isAdmin, matrixData.find({}).count());
    if(user && user.isAdmin) return matrixData.find({});
});
// App B: /imports/api/startup/both/index.js
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { DDP } from 'meteor/ddp-client'

var remoteUrl = 'http://localhost:3000';
Meteor.remoteConnection = DDP.connect(remoteUrl);
Accounts.connection = Meteor.remoteConnection;

Meteor.users = new Mongo.Collection('users', {
    connection: Meteor.remoteConnection
});
// __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL = remoteUrl;
// ^^ tried this from a post jamgold posted, but commented out for now. doesn't seem to make a difference. :( ```

// App B: /imports/api/collections.js
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';

export var matrixData = new Mongo.Collection('matrixData', {
    connection: Meteor.remoteConnection
});

And relevant bits in matrix.js:

// App B: /imports/ui/matrix.js
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { 
    matrixData
} from '/imports/api/collections.js';
import './matrix.html';
Template.Matrix.onRendered(function(){
    Meteor.remoteConnection.call('isAdmin', function(err, res){ 
        if(res){ // method returns boolean
            Meteor.remoteConnection.subscribe('matrixData');
        }
    }
});
Template.Matrix.helpers({
    mData: function(){
        var data = matrixData.find({});
        console.info(matrixData.find({}).count()); // always 0!
        return data.count();
    }
});

Being an Admin app, App B requires authentication.

// App B: /imports/ui/home.js
Meteor.loginWithPassword(email, password, function(err, res) {
    if (!err) {
        FlowRouter.go('matrix');
    }
});

Above works great on App B and I can see the login event fires on App A’s side. (One issue is that once I’m logged in, a reload kicks me out and I’d love it if anyone can provide insight into how to keep the authentication persistent across browser reloads.)

On login to App B, I’m redirected to the matrix view. Then Meteor.remoteConnection.call('isAdmin' successfully fires, which performs security checks on App to be sure I’m a privledged Admin. If I am, the subscriptions begin.

Here’s the strange part: in App A (/imports/api/server/pubs.js) above, you can see I’m console logging some information. When the subscription to matrixData fires, I can successfully see this in App A’s server-side console:

'matrixData' '8fKtRBPXZis2ZrZm2' true 39

So it seems the subscription is clearly being subscribed to on App B’s side, and the “39” is the number of documents in that collection. But on App B’s side, I get zero documents every time. I’ve tried to disable browser security policies, I have zero pubs defined on App B as well as zero Methods on App B.

Can anyone spot what I’m missing? Why is App A showing I’m subscribing to a Collection with N documents, but App B can’t get even a single document from the same Collection?

1 Like

Additional info: based on this post (Share login state between microservices), I was able to modify that code to enable a reload that doesn’t cause a logout.

Full code here (note: it’s the same code above but simply extended):

// App B: /imports/api/startup/both/index.js
import { Meteor } from 'meteor/meteor';
import { Accounts, AccountsClient } from 'meteor/accounts-base';
import { DDP } from 'meteor/ddp-client'

var remoteUrl = 'http://localhost:3000';
Meteor.remoteConnection = DDP.connect(remoteUrl);
Accounts.connection = Meteor.remoteConnection;

Meteor.users = new Mongo.Collection('users', {
    connection: Meteor.remoteConnection
});
var loggingIn = false;
Accounts.onLogin(() => {
    var loginToken = Accounts._storedLoginToken();
    // Use the accounts loginToken to login 
    if (loginToken) {
        if(!loggingIn){
            loggingIn = true; // prevents throttling when .onLogin re-runs multiple times
            Meteor.loginWithToken(loginToken, function(loginErr, result){
                if (loginErr) {
                    console.log(loginErr.message);
                } else {
                    if (result.type === 'resume') {
                        FlowRouter.go('matrix');
                    }
                }
            });
        }
    } else {
        console.log('no log in token');
    }
});

// __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL = remoteUrl;
// ^^ this doesn't seem to make a difference 

Not only does that ensure logins remain through a reload, but I’m also successfully able to subscribe to the users collection on App A. I can see the user total as 2 (which is my test users collection count), and if I add another user it reactively changes to 3, as I’d expect from a subscription change.

So I’m able to at least get one subscription working, but I’m still at a loss for why the others seem to actually “subscribe” (because i can see App A report the subscription), but fail to retrieve even a single document.

1 Like

HI @mikeTT,

The topic here discusses the problem you are trying to address.

There is some confusion in your code that the writeup may help you to sort out…

Couple of things I noticed

You need to return a this.ready() from your publish for non-admins:

if(user && user.isAdmin) return matrixData.find({});
else this.ready();

Are you using the accounts package? If so, this is automatically set up by the package and you should not do it:

Meteor.users = new Mongo.Collection('users', {
    connection: Meteor.remoteConnection
});

Thanks, @brucejo. Glad to see you chime in, as I read your posts previously to get my solution working.

With respect to this.ready();, it’s a good call but with these publications in particular, they’re not to be accessed except by Admins. So if a random user somehow knew the pub name, we just want to bail and return nothing if they’re not an Admin.

On the new Mongo.Collection('users'..., I found that if I rely on Accounts package, it’ll create a local users collection (with only one user, which is me). What I wanted to happen is to use the remote collection, and by setting the connection property it avoids the local collection and gives me data from the main db (i.e. App A’s collection).

I ended up getting the functionality to work. Ultimately it was somewhat embarrassingly simple. In the order of all my imports, I had been declaring on both server and client the Meteor.remoteConnection. I thought by doing so, the .remoteCollection property would be global on Meteor and I could thus use it everywhere. Turns out I just needed to create the DDP connection, then import that connection into my collections.js file so as to connect it to App A’s db.

With respect to this.ready(); , it’s a good call but with these publications in particular, they’re not to be accessed except by Admins. So if a random user somehow knew the pub name, we just want to bail and return nothing if they’re not an Admin.

Oh sorry, didn’t mean to say else return this.ready(); should just be else this.ready();. Repaired in previous comment. However, your thought is a clever one as long as you are never subscribing to the collection from any user other than admin, the client would act as if the publication did not exist.

Regarding the accounts package, I see what you are doing. If you ever need it, the accounts package can work if you share server code that publishes the users you want published. That is how I have been doing it for simplicity on my servers.

1 Like

Indeed, for many security and authorization checks, the operating assumption is they’ll very likely not fail if it’s a valid/appropriate user. If it fails, it’s likely that it’s just somebody doing things they’re not supposed to do/see, so I report very little to the client to give them no insight into what the server experienced.

or you can return this.stop or this.error to tell sub require auth.

i think you should either return this.stop or this.error to tell client Meteor.subscribe status

The idea with these specific publications is that they should not be discoverable nor subscribe-able unless someone has the right authorization priviledges. So from a security perspective, I report nothing back to the client. Basically the subscription attempt just fails with no reason as to why, giving the malicious user no idea what failed. For all legitimate users, this won’t be an issue since they won’t be trying to subscribe to things they’re not allowed.

1 Like