Hi I am rate limiting some methods in meteor.js with DDPRateLimiter and what I found out is that it limits method call for everyone not just that connection! For example for this rule:
var updateUsernameRule = {
type: 'method',
name: 'updateUsername'
};
DDPRateLimiter.addRule(updateUsernameRule, 1, 30000);
Update in one browser and then when updating in another it causes rate limit error. Documentation is kinda unclear about that and default behavior is not intuitive at all. How do I rate limit per user?
I agree, the docs need a bit of work. To make this work in your case (restrict by logged in user only), you will want something like the following:
const updateUsernameRule = {
type: 'method',
name: 'updateUsername',
userId(userId) {
if (userId) {
return true;
}
}
};
DDPRateLimiter.addRule(updateUsernameRule, 1, 30000);
This will cause the updateUsernameRule rule to only be matched for user's with a set userId (logged in users). All other not logged in users will skip this rule completely, and be able to make as many requests as they want.
Related
I'm wondering if this is possible, and if it's a good solution to my problem.
I want users to be able to subscribe to content. The content is associated with an id.. for instance:
'JavaScript': 1,
'C++': 2,
'Python': 3,
'Java': 4,
Let's say a user subscribes to 1, 3, and 4.
So their user json data would appear as:
'subscribed_to': [1,3,4]
Now in my firestore, I have posts. Each post gets assigned a content_id (1-4 for instance), and so when I query for the content that this user is subscribed to, how would I do that so as effectively as possible?
This is indeed a complex but common case, I would recommend to set a data structure similar to:
{
"subscriptions" {
javascript: { ... },
python: { ... },
java: { ... }
},
"users": {
1: {
subscribed_to: ['javascript', 'python']
}
}
}
It's very important that on your subscribed_to prop you use the doc name, cause this is the part that allows you to query them (the docs).
the big problem, how do I query this data? I don't have joins!
Case 1:
Assuming you have your user data when you apply load...
const fetchDocs = async (collection, docs) => {
const fetchs = docs.map(id => collection.doc(id).get())
const responses = await Promise.all(fetchs)
return responses.map(r => r.data())
}
const userSubscriptions = ['javascript', 'python']
const mySubscriptions = await fetchDocs(`subscriptions`, userSubscriptions)
Behind the scene, the sdk will group all the queries and do its best efforts to deliver em together. This works good, I'm 99% sure you still have to pay for each query individually each time the user logs in.
Case 2:
Create a view dashboard collection and pre-calculate the dashboard behind the scene, this approach involves cloud functions to listen or changes on the user changes (and maybe subscriptions as well) and copy each individual doc into another collection, let's say subscriptions_per_users. This is a more complex approach, I will require more time to explain but if you have a big application where costs are important and if the user is going to subscribe to a lot of things, then you might want to investigate about it.
Source: My own experience... a little of google can also help, there seems to be a lot of opinions about it, find what works best for you.
Use Case Summary
User A creates a story
User A shares story with unknown (to the app) User B via email (sent via cloud function)
User B receives an email about the story
User B visits app and creates a new account
User B sees/reads story create by User A
Note: stories can only be seen by whom they been shared with or created by
I'm building a role based access system. I've been looking at the role based access firestore documentation and I'm missing one piece.
Consider a story that can only be read by a user for which that story has been shared. Most examples including the firestore example use the UID has the key to identify shared users. However, that user may not currently be a user of the firebase app additionally how does a user assign that UID.
Story Data
{
title: "A Great Story",
roles: {
aliceUID: {
hasRole: true,
type: "owner",
},
bobUID: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.uid}.hasRole`, '==', true)
The second part could potentially be solved by maintaining a separate user collection then you could find the user from their email address, but this doesn't address users that have never logged in.
The user who intends to share a story could add the user with an email address. Then using firebase functions we could send an email to notify the user of the shared story and the user could login to the app and read that story.
If we proceed with this method then you would not have a UID but only an email address as the key.
Story Data
{
title: "A Great Story",
roles: {
alice#yahoo.com: {
hasRole: true,
type: "owner",
},
bob#gmail.com: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.email}.hasRole`, '==', true)
Updated Firestore Rule - from documentation
function getRole(rsc) {
// Read from the "roles" map in the story document.
return rsc.data.roles[request.auth.uid] || rsc.data.roles[request.auth.token.email];
}
I can not get the email query to work. This SO issue mentions that
Unfortunately dots are not allowed as a map key. So email addresses won't work.
I don't see why this would be a conflict on the rules side. It does make for a likely invalid where clause
e.g.
.where(`roles.${user.email}.hasRole`, '==', true) -> .where(`roles.bob#gmail.com.hasRole`, '==', true)
That looks like invalid JS and unfortunately [ and ] are invalid characters so we can't do
.where(`roles[${user.email}]hasRole`, '==', true)
The final thing I've seen is using for this Firebase talk is to escape the email address using something like
function encodeAsFirebaseKey(string) {
return string.replace(/\%/g, '%25')
.replace(/\./g, '%2E')
.replace(/\#/g, '%23')
.replace(/\$/g, '%24')
.replace(/\//g, '%2F')
.replace(/\[/g, '%5B')
.replace(/\]/g, '%5D');
};
This appears to fix the query where clause and it's a valid data structure, but it's not a valid Firestore rule meaning it has no true security enforcement.
Any ideas on how to implement this? Please include valid data structure, firestore rules, and query. I've shown and seen many examples that get two out of the three which are all non-working solutions.
Thanks!
The basic issue was that I did not know how to properly formulate a valid query. It turns out that you don't need to create a query in one line.
You can use FieldPath to construct your query parameter.
var path = new firebase.firestore.FieldPath('roles', email ,'hasRole');
firebase.firestore().collection('stories').where(path, '==', true)
This solves for the original missing piece.
This is a use case for Control Access with Custom Claims and Security Rules.
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
User roles can be defined for the following common cases:
Giving a user administrative privileges to access data and resources.
Defining different groups that a user belongs to.
Providing multi-level access:
Differentiating paid/unpaid subscribers.
Differentiating moderators from regular users.
Teacher/student application, etc.
You'll need to stand up a node server (skill level low). A script like below works to generate the claims.
var admin = require('firebase-admin');
var serviceAccount = require("./blah-blah-blah.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://my-app.firebaseio.com"
});
admin.auth().setCustomUserClaims("9mB3asdfrw34ersdgtCk1", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
Then on your client side, do something like:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
//is email address up to date? //do we really want to modify it or mess w it?
switch (user.providerData[0].providerId) {
case 'facebook':
case 'github':
case 'google':
case 'twitter':
break;
case 'password':
// if (!verifiedUser) {
// }
break;
}
//if admin
firebase.auth().currentUser.getIdToken().then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin or whatever
if (!!payload['admin']) {
switch (thisPage) {
case "/admin":
showAdminStuff();
break;
}
}
else {
if(isAdminPage()){
document.location.href="/";
}
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});
From what I have gathered, you want to make a story private but shareable with anyone. Your biggest concern is for users who do not have the app but have the share link.
And therefore your biggest problem is that the way firebase works means that you cant limit access to your data without using some sort of login.
If you are ok with requiring new users to login, then your answer should just be Dynamic Links. These links are persistent all the way though installation and login which means that anyone can be given a dynamic link that has story access data attached. You would merely need to add a listener to your app's mainActivity or AppDelegate equivalent to record the dynamic link data and run a specif task after login.
If you wish to stay away from the login completely, then you set up the dynamic link to bypass the login process and direct the new-install-user directly to the story. This second option however, requires a bit more work and is less secure because you will probably be forced to duplicate the story data for open access to anyone with the proper link to the story.
I have a set of users defined like this:
Accounts.createUser({
username:'Simon',
email:'simon#email.com',
profile:{
firstname:'Simon',
lastname:'Surname',
location:'Home Address',
privacy: {
location:0,
emails:0 } //Location and emails are private and should not be disclosed
}
});
My question is how can I publish this user's record for other users to view, taking into account the profile privacy settings. In this example, I have set the privacy for location and emails to zero with the intention that this information is not published for this user.
I would like to publish it using the standard method:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find();
});
But I cannot see a way to specify the selector or fields in such a way that only public information will be published.
I have tried adding additional publications of the form:
Meteor.publish("allUsers", function () {
return Meteor.users.find( {}, {fields:{username:1}} );
});
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, emails:1}} );
});
but the selector does not seem to be returning the emails as I expected. I am looking for optimal way to do this from a performance point of view.
Mongodb is not a relational database so whenever I want to join or query based on metadata I remember I have to do things differently. In your case I would make a separate Collection for user privacy if I wanted to query on user privacy. In addition, if I cared about performance I probably would never want "all of x", I would just want enough to show the user, thus paginate. With these two ideas in mind you can easily get what you want: query based on privacy settings and performance.
Privacy = new Mongo.Collection("privacy");
Whenever we want to add privacy to an account:
Privacy.insert({
emails: 1,
userId: account._id,
});
Then later, one page at a time, showing ten results each page, tracking with currentPage:
Meteor.publish("usersWithPublicEmails function (currentPage) {
var results = []
var privacyResults = Privacy.find({"emails":1}, {skip: currentPage,
limit: 10});
var result;
while (privacyResults.hasNext() ) {
result = privacyResult.next();
results.append(Meteor.users.find({_id: result.userId});
}
return result;
});
I didn't test this code, it may have errors, but it should give you the general idea. The drawback here is that you have to keep privacy and users in sync, but these are the kinds of problems you run into when you're not using a relational database.
Mongodb has a way to do this kind of reference lookup with less code, but it still happens on demand and I prefer the flexibility of doing it myself. If you're interested take a look at Database references
That's because you have a typo in your publish function's fields object, instead of email you've typed emails
So the correct function would be:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, email:1}} );
});
Furthermore, you're already publishing all usernames in your allUsers publication, therefore, in order to add the missing data for relevant public users, you'll just need this:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{email:1}} );
});
and Meteor will automatically merge those records for you.
A simple solution in the end. I had missed the additional subscription in my router:
Router.route('/users', {
name: 'userList',
waitOn: function(){
return Meteor.subscribe('allUsers') &&
Meteor.subscribe('usersWithPublicEmails');
},
data: function(){
return Meteor.users.find();
}
});
A basic mistake:-(
I have a meteor collection like this:
Cases = new Meteor.Collection('cases');
As well i have registered users (max 10). I now want to be able to "give" a single case to a registered user and be sure, that no other user is getting that specific case.
The User is working with the case (updating fields, deleting fields) and then sends it in some kind of archive after submitting the user should get a new case that is in the collection.
My thought was to have field called "locked" which initially is set to false and in the moment it is displayed at the user "locked" gets true and is not returned anymore:
return Cases.find({locked: false, done: false}, {limit: 1});
Any ideas how to do that in meteor?
Thanks
You just need to attach an owner field (or similar) to the case. That would allow you to do things like:
Only publish the case to the user who is also the owner using something like:
Meteor.publish('cases/unassigned', function() {
return Cases.find({owner: {$exists: false}});
});
Meteor.publish('cases/mine', function() {
return Cases.find({owner: this.userId});
});
Not allow a user to update or delete a case if it's not assigned to them:
Cases.allow({
update: function(userId, fieldNames, doc, modifier) {
return userId === doc.owner;
},
delete: function(userId, doc) {
return userId === doc.owner;
}
});
Obviously, these would need amending for stuff like super-users and you probably need some methods defined to allow users to take cases, but that's the general idea.
There are concurrency issues to deal with, to reliably allocate a case to only one person.
We need to solve two things:
1. Reliably assign the case to a user
2. Fetch the cases assigned to a user
Number 2. is easy, but depends on 1.
To solve 1., this should work:
var updated = Cases.update(
{_id: <case-to-assign>, version: "ab92c91"},
{assignedTo: Meteor.userId(), version: Meteor.Collection.ObjectID()._str});
if (updated) {
// Successfully assigned
} else {
// Failed to assign, probably because the record was changed first
}
Using this you can query for all of a users cases:
var cases = Cases.find({assignedTo: Meteor.userId()});
If 10 people try get a case at the same time, it should have a pre-set version field, and the MongoDB will only let the .update work once. As soon as the version field changes (due to an .update succeeding) the remaining updates will fail as the version field could no longer match.
Now that the allocation has taken place reliably, fetching is very simple.
As suggested by #Kyll, the filtering of cases should be done inside a Meteor publication.
It would also make sense to perform the case-assignment inside a Meteor method.
UPDATE:
#richsilv's solution is simpler than this one, and works fine.
This solution is useful if you need to know who won immediately, without making further requests to the server.
Is it possible to simply display a number of online users (currently logged in) with meteor-presence?
I see that documents in Meteor.presences collection do not delete themselves by default, so simple Meteor.presences.find().count() is not the value I'm looking for... Should I delete them manually or should I check if each document has userID field somehow?
EDIT:
To illustrate a bit more what is happening to me, I tried to log in with 2 different users in 2 different browsers. After calling Meteor.presences I can see two documents in my collection, e.g.
docs: {
djF3noxe3AhxDRfZw: {
_id: "djF3noxe3AhxDRfZw",
state: "online",
userId: "SDeLPJzoabFt4Knei"
}, {
t4r2Q7KGKji4FPS9s: {
_id: "t4r2Q7KGKji4FPS9s",
state: "online",
userId: "5zvYoC37aXSADGNEg"
}
}
After few minutes of inactivity, both docs look the same. After logging out (calling Meteor.logout()) with one user I still have 2 documents (even after few minutes), the only difference is that one of them has no more userID:
docs: {
djF3noxe3AhxDRfZw: {
_id: "djF3noxe3AhxDRfZw",
state: "online"
}, {
t4r2Q7KGKji4FPS9s: {
_id: "t4r2Q7KGKji4FPS9s",
state: "online",
userId: "5zvYoC37aXSADGNEg"
}
}
Only after closing that browser window the associated document deletes itself.
So, if I call Meteor.presences.find().count() when both windows are opened, I always get 2 in return, no matter of user's online/offline status.
All the code I am using at the moment is the one from the example:
Meteor.publish('userPresence', function() {
var filter = {};
return Meteor.presences.find(filter, {fields: {state: true, userId: true}});
});
I was seeing the same thing and it confused me as well. But when you think about it, a visitor looking at the page is a presence as well, yet doesn't have a userId. So it makes sense that any visitor would show up in the collection.
If you only want to see logged in users you could change filter to
var filter = {userId: {$exists: true}};
You can do this easily with the package I've created, https://github.com/mizzao/meteor-user-status. It doesn't double-count users, and you can just display or publish the value
Meteor.users.find({ "status.online": true }).count()
It also updates immediately when users disconnect or log off.
The documents delete themselves after a timeout period (10 seconds). https://github.com/tmeasday/meteor-presence/blob/master/presence_server.js#L1