Node Fibers Trouble with Meteor - javascript

I'm trying to write a simple authentication backend for Meteor that authenticates against an LDAP server. I need the function registered as login handler (the input to Accounts.registerLoginHandler) to return the id the of the user just logged in.
The problem I think lies in the Fiber I've created, getUserId, and that it's not returning id like I want it to. I know it has to be in a Fiber or else meteor gets angry and throws errors. Even though the log right before the yield shows me the id isn't undefined, getUserId.run() always returns undefined.
Any help would be greatly appreciated, thanks!
Accounts.registerLoginHandler(function(loginRequest) {
console.log("In login handler");
return auth.authenticate(loginRequest.username, loginRequest.password, function(err, ldap_user) {
if (err){
// ldap authentications was failed
console.log("Login failed");
return undefined;
}
else {
// authentication was successful
console.log("Login success");
// extracting team name from ldap record
var equals = ldap_user.memberOf.indexOf("=");
var comma = ldap_user.memberOf.indexOf(",");
var team_name = ldap_user.memberOf.slice(equals+1,comma);
// add user if they don't already exist
var getUserId = Fiber( function() { // Meteor code must be ran within a fiber
var id = null;
var user = Meteor.users.findOne({username: loginRequest.username});
if(!user) {
// insert user and kick back id
id = Meteor.users.insert({username: loginRequest.username,
profile : {team : team_name}
});
console.log('no user found, creating' + id);
} else {
id = user._id;
console.log('user found, returning id' + id);
}
console.log('id: '+id);
Fiber.yield(id); // return id
});
// send logged in users if by executing the fiber
return {id: getUserId.run()};
}
});
});

I think the problem is related to instead needing to use Meteor.bindEnvironment to control the scope of the (environment) variables and fibers in use.
A good three-step tutorial on the subject is found here:
https://www.eventedmind.com/feed/nodejs-introducing-fibers
https://www.eventedmind.com/feed/meteor-dynamic-scoping-with-environment-variables
https://www.eventedmind.com/feed/meteor-what-is-meteor-bindenvironment
My take on your code would be something like this (which in a similar problem worked for me):
Accounts.registerLoginHandler(function(loginRequest) {
console.log("In login handler");
var boundAuthenticateFunction = Meteor.bindEnvironment(function(err, ldap_user) {
if (err){
// ldap authentications was failed
console.log("Login failed");
return undefined;
}
else {
// authentication was successful
console.log("Login success");
// extracting team name from ldap record
var equals = ldap_user.memberOf.indexOf("=");
var comma = ldap_user.memberOf.indexOf(",");
var team_name = ldap_user.memberOf.slice(equals+1,comma);
// add user if they don't already exist
var id = null;
var user = Meteor.users.findOne({username: loginRequest.username});
if(!user) {
// insert user and kick back id
id = Meteor.users.insert({username: loginRequest.username,
profile : {team : team_name}
});
console.log('no user found, creating' + id);
} else {
id = user._id;
console.log('user found, returning id' + id);
}
console.log('id: '+id);
return {id: id};
}
}, function(e){throw e;});
return auth.authenticate(loginRequest.username, loginRequest.password, boundAuthenticateFunction);
});
Mind, the code sample above is untested...

Related

Firebase Functions - Return after Realtime Database fetched inside User Fetch

I have a Firebase Cloud Function that is called in my app with JavaScript.
When the function is called it fetches the user data from the user ID, then fetched a record from the Realtime Database to check for a match.
This function works but is returning "null" and finishing early instead of returning the success or error message when the match is detected.
How can I make the return text be the success or error from the match and only complete once this match is decided?
exports.matchNumber = functions.https.onCall((data, context) => {
// ID String passed from the client.
const ID = data.ID;
const uid = context.auth.uid;
//Get user data
admin.auth().getUser(uid)
.then(function(userRecord) {
// Get a database reference to our posts
var db = admin.database();
var ref = db.ref("path/to/data/" + ID);
return ref.on("value", function(snapshot) {
//Fetch current phone number
var phoneORStr = (snapshot.val() && snapshot.val().phone) || "";
//Fetch the current auth user phone number
var userAuthPhoneNumber = userRecord.toJSON().phoneNumber;
//Check if they match
if (userAuthPhoneNumber === phoneORStr) {
console.log("Phone numbers match");
var updateRef = db.ref("path/to/data/" + ID);
updateRef.update({
"userID": uid
});
return {text: "Success"};
} else {
console.log("Phone numbers DO NOT match");
return {text: "Phone number does not match the one on record."};
}
}, function (errorObject) {
console.log("The read failed: " + errorObject.code);
return {text: "Error fetching current data."};
});
})
.catch(function(error) {
console.log('Error fetching user data:', error);
return {text: "Error fetching data for authenticated user."};
});
});
Thank you
The Firebase ref.on() method doesn't return a promise, so the return statements you have in there do nothing.
You're looking for ref.once(), which returns a promise, and thus will bubble up the return statements you have within it:
return ref.once("value").then(function(snapshot) {
...
As Doug pointed out, you'll also need to return the promise from the top level. So:
//Get user data
return admin.auth().getUser(uid)
.then(function(userRecord) {

JavaScript function response and chained promises

I have a Parse CloudCode beforeSave function, which roughly does the following:
Runs a query to check if there's a duplicate user to the one being saved;
If there is NO duplicate, I call a response.success(), which means the code can go ahead and allow saving the new user;
If, however, there is a duplicate, I want to take the existing user, fetch a group object and add the existing user to the group.
For this purpose, I am using chained promises to make the code cleaner. The code is below:
Parse.Cloud.beforeSave("Contact", function(request, response) {
var facebookID = request.object.get("facebookID");
var email = request.object.get("email");
var queryFb;
if (facebookID) {
queryFb = new Parse.Query(Parse.User);
queryFb.equalTo("facebookID", facebookID);
}
var queryEmail;
if (email) {
queryEmail = new Parse.Query(Parse.User);
queryEmail.equalTo("email", email);
}
var query;
if (facebookID && email) {
query = new Parse.Query.or(queryFb, queryEmail);
} else if (facebookID) {
query = queryFb;
} else {
query = queryEmail;
}
var user;
query.first().then(function(user) {
if (!user) {
response.success();
} else {
var groupQuery = new Parse.Query("Group");
groupQuery.equalTo("title", "ssAll");
groupQuery.equalTo("owner", request.user);
return groupQuery.first();
}
}).then(function(group) {
group.addUnique("contacts", user);
return group.save();
}).then(function(success) {
response.error("NOT ERROR - new object was NOT created");
}, function(error) {
response.error(error);
});
});
In my test case, the query returns !user, so the response.success() message is called - all good. However, this response seems to then travel down the promise chain, which is intended for the case when the query returns a user object. And so, my function terminates with an error on line group.addUnique("contacts", user); because, obviously, the group object is undefined.
How do I work around this issue?
The code needed a few improvements. The key improvement was to provide consistent starting conditions to the second promise's resolution (the second then block). The OP code called response.success() in the case where there was no existing user. This is fine, except the execution still falls through to the next resolution, in one case with an undefined group parameter.
The new code fixes that by returning either the existingUser (after the group has been updated) or null. Null tells the next promise resolution to call success() and allow the save to proceed, otherwise, block the save.
Also note, it is a mistake for the first block's user parameter to conflict with the var user in the enclosing scope. I tried to use variable naming below to highlight the two different types of users the code considers...
Parse.Cloud.beforeSave("Contact", function(request, response) {
var facebookID = request.object.get("facebookID");
var email = request.object.get("email");
// moved into a function so we can test and deal with it tersely
findUserByEmailOrFB(email, facebookID).then(function(existingUser) {
return (existingUser)? addContactToGroupOwnedBy(request.user, existingUser) : null;
}).then(function(existingUser) {
if (existingUser) {
response.error("NOT ERROR - new object was NOT created");
} else {
response.success();
}
}, function(error) {
response.error(error);
});
});
// find the group owned by ownerUser, add contactUser to its contacts return a promise fulfilled as contactUser
function addContactToGroupOwnedBy(ownerUser, contactUser) {
var groupQuery = new Parse.Query("Group");
groupQuery.equalTo("title", "ssAll");
groupQuery.equalTo("owner", ownerUser);
return groupQuery.first().then(function(group) {
group.addUnique("contacts", contactUser);
return group.save().then(function() { return contactUser; });
});
}
function findUserByEmailOrFB(email, facebookID) {
var queryFb;
if (facebookID) {
queryFb = new Parse.Query(Parse.User);
queryFb.equalTo("facebookID", facebookID);
}
var queryEmail;
if (email) {
queryEmail = new Parse.Query(Parse.User);
queryEmail.equalTo("email", email);
}
var query;
if (facebookID && email) {
query = new Parse.Query.or(queryFb, queryEmail);
} else if (facebookID) {
query = queryFb;
} else {
query = queryEmail;
}
return query.first();
}
The problem is that you are always resolving the first promise regardless of whether checking for the user was successful (no such user yet) or not. However, actually you don't ever have to resolve the promise. I suggest you separate the error case like this:
query.first().then(function(user) {
if (!user) {
response.success();
} else {
addUserToGroup(request.user).then(function() {
response.error("NOT ERROR - new object was NOT created");
}, function(error) {
response.error(error);
});
}
});
function addUserToGroup(user) {
var groupQuery = new Parse.Query("Group");
groupQuery.equalTo("title", "ssAll");
groupQuery.equalTo("owner", user);
return groupQuery.first().then(function(group) {
group.addUnique("contacts", user);
return group.save();
});
}
As you can see, the first promise doesn't ever have to be resolved because the result is not used anyway.

Linking Google account with existing account created using email in Parse.com

I have implemented google login in parse. Here is my code:
var querystring = require('querystring');
var _ = require('underscore');
var Buffer = require('buffer').Buffer;
var googleValidateEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo';
var TokenStorage = Parse.Object.extend("TokenStorage");
var restrictedAcl = new Parse.ACL();
restrictedAcl.setPublicReadAccess(false);
restrictedAcl.setPublicWriteAccess(false);
Parse.Cloud.define('accessGoogleUser', function(req, res) {
var data = req.params;
var token = data.code;
/**
* Validate that code and state have been passed in as query parameters.
* Render an error page if this is invalid.
*/
if (!(data && data.code)) {
res.error('Invalid auth response received.');
return;
}
Parse.Cloud.useMasterKey();
Parse.Promise.as().then(function() {
// Validate & Exchange the code parameter for an access token from Google
return getGoogleAccessToken(data.code);
}).then(function(httpResponse) {
var userData = httpResponse.data;
if (userData && userData.id) {
return upsertGoogleUser(token, userData, data.email);
} else {
return Parse.Promise.error("Unable to parse Google data");
}
}).then(function(user) {
/**
* Send back the session token in the response to be used with 'become/becomeInBackground' functions
*/
res.success(user.getSessionToken());
}, function(error) {
/**
* If the error is an object error (e.g. from a Parse function) convert it
* to a string for display to the user.
*/
if (error && error.code && error.error) {
error = error.code + ' ' + error.error;
}
res.error(JSON.stringify(error));
});
});
var getGoogleAccessToken = function(code) {
var body = querystring.stringify({
access_token: code
});
return Parse.Cloud.httpRequest({
url: googleValidateEndpoint + '?access_token=' + code
});
}
var upsertGoogleUser = function(accessToken, googleData, emailId) {
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first({ useMasterKey: true }).then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var user = tokenData.get('user');
return user.fetch({ useMasterKey: true }).then(function(user) {
// Update the access_token if it is different.
if (accessToken !== tokenData.get('accessToken')) {
tokenData.set('accessToken', accessToken);
}
/**
* This save will not use an API request if the token was not changed.
* e.g. when a new user is created and upsert is called again.
*/
return tokenData.save(null, { useMasterKey: true });
}).then(function(obj) {
// Reset password
password = new Buffer(24);
_.times(24, function(i) {
password.set(i, _.random(0, 255));
});
password = password.toString('base64')
user.setPassword(password);
return user.save();
}).then(function(user) {
// ReLogin
// This line is what I am talking about
return Parse.User.logIn(user.get('username'), password);
}).then(function(obj) {
// Return the user object.
return Parse.Promise.as(obj);
});
});
}
var newGoogleUser = function(accessToken, googleData, email) {
var user = new Parse.User();
// Generate a random username and password.
var username = new Buffer(24);
var password = new Buffer(24);
_.times(24, function(i) {
username.set(i, _.random(0, 255));
password.set(i, _.random(0, 255));
});
var name = googleData.name;
// name = name.split(" ");
// var fullname = name;
// if(name.length > 1)
// var lastName = name[name.length-1];
user.set("username", username.toString('base64'));
user.set("password", password.toString('base64'));
user.set("email", email);
user.set("fullName", name);
// user.set("last_name", lastName);
user.set("accountType", 'google');
// Sign up the new User
return user.signUp().then(function(user) {
// create a new TokenStorage object to store the user+Google association.
var ts = new TokenStorage();
ts.set('user', user);
ts.set('accountId', googleData.id);
ts.set('accessToken', accessToken);
ts.setACL(restrictedAcl);
// Use the master key because TokenStorage objects should be protected.
return ts.save(null, { useMasterKey: true });
}).then(function(tokenStorage) {
return upsertGoogleUser(accessToken, googleData);
});
}
It works perfectly fine. Now the problem I am facing is that I want to link google account with an existing parse account created using email or username & password. The problem in doing so is that to login/signup using google I have to reset the password of the user to login so as to get the session token. See this line in the code -> [This line is what I am talking about]. So if I do so an existing user who had earlier used username/email & password to login won't be able to login again using email since I have reset his/her password. I have seen this and all the other links related to this but none of which solves this problem.
Can somebody here guide me in the right direction?
Log added as response to one of the comments:
{"accountType":"google","createdAt":"2016-01-07T17:30:57.429Z","email":"skdkaney#gmail.com","fullName":"ashdakhs basdkbney","updatedAt":"2016-01-07T17:30:57.429Z","username":"owt3h0ZZEZQ1K7if55W2oo3TBLfeWM6m","objectId":"lSlsdsZ9"}
Added upsert function as per comment request:
var upsertGoogleUser = function(accessToken, googleData, emailId) {
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first({ useMasterKey: true }).then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var userw = tokenData.get('user');
var users_id = userw.id;
var query2 = new Parse.Query(Parse.User);
query2.equalTo('objectId',users_id);
// The new query added
return query2.first({ useMasterKey: true }).then(function(user) {
// Update the access_token if it is different.
// if (accessToken !== tokenData.get('accessToken')) {
// tokenData.set('accessToken', accessToken);
// }
console.log(user);
console.log("******");
/**
* This save will not use an API request if the token was not changed.
* e.g. when a new user is created and upsert is called again.
*/
// return tokenData.save(null, { useMasterKey: true });
}).then(function(obj) {
console.log(obj);
// console.log(user);
var result = user ;
// Return the user object.
return Parse.Promise.as(result); // this is the user object acquired above
});
After a discussion with OP, there are possible solutions to this matter but each of them have pros and cons.
Disabling Revocable Session
Since the introduction of Revocable Session, getSessionToken will always return undefined even with master key. To turn it off, go to App Settings >> Users >> Turn off Require revocable sessions.
Then, in upsertGoogleUser method, you just need to return the user object from tokenData.get('user'). It is enough to call user.getSessionToken() in your main cloud function. The final method should look like:
var upsertGoogleUser = function(accessToken, googleData, emailId) {
Parse.Cloud.useMasterKey();
var query = new Parse.Query(TokenStorage);
query.equalTo('accountId', googleData.id);
//query.ascending('createdAt');
// Check if this googleId has previously logged in, using the master key
return query.first().then(function(tokenData) {
// If not, create a new user.
if (!tokenData) {
return newGoogleUser(accessToken, googleData, emailId);
}
// If found, fetch the user.
var userw = tokenData.get('user');
var users_id = userw.id;
var query2 = new Parse.Query(Parse.User);
query2.equalTo('objectId',users_id);
return query2.first().then(function(user) {
console.log(user);
console.log(user.getSessionToken());
console.log("******");
return Parse.Promise.as(user);
});
});
};
User Password Input
In order not to change user's password, we can ask user to input his password once we successfully authenticated Google data. We then use the input password to log user in. This is not a good UX, since the purpose of Google Login is to increase usability by letting users not entering password.
Query on Parse.Session
This is a possible solution if you want to use "Revocable Session" feature. In the code above, instead of querying on Parse.User, we can look for any revocable session in Parse.Session class. Then we can call getSessionToken on returned object. This is not optimal solution in cases that we need to know which devices the user is logged in.
Reference:
Parse's Enhanced Session: http://blog.parse.com/announcements/announcing-new-enhanced-sessions/

Not able to save a custom class object in Parse

I am trying to create a Customer class object which has a one to one relation with the User class. But the object doesn't save without giving any error.
Here is my code:
Parse.Cloud.afterSave(Parse.User, function(request) {
user = request.object;
role_name = user.get("role_name");
user_name = user.get("user_name");
user_id = user.get("objectId");
if (role_name == "customer"){
user = request.object;
console.log(" I am inside if else");
var Customer = Parse.Object.extend("Customer");
var cus = new Customer();
cus.set("name2" , "albert")
var relation = cus.relation("userId");
relation.add(user);
cus.save(); // Customer object should get saved here
cus.save(null, {
success: function(cus) {
// Execute any logic that should take place after the object is saved.
console.log("I am working")
alert('New object created with objectId: ' + cus.objectId);
},
error: function(error) {
// handleParseError(error);
console.log(error)
// Execute any logic that should take place if the save fails.
// error is a Parse.Error with an error code and message.
}
});
}
});
Log when I run this:
after_save triggered for _User for user qu808uKOgt:
Input: {"object":{"createdAt":"2015-10-11T18:36:07.661Z","objectId":"qu808uKOgt","phone":"5678956475","role_name":"customer","updatedAt":"2015-10-11T18:36:07.661Z","username":"newuser16"}}
Result: Success
I am inside if else
{"name2":"apple","userId":{"__op":"AddRelation","objects": [{"__type":"Pointer","className":"_User","objectId":"qu808uKOgt"}]}}
I fixed this bug by creating a new cloud function, which will be called immediately after the user sign's up.
Parse.Cloud.define('functionName', function(request, response){
var currentUser = Parse.User.current();
var role_name = currentUser.get("role_name");
if (role_name == "customer"){
// Do something
}
else if (role_name == "service_provider"){
// Do something else
}
)};

Exception in async function: Only on server, not on localhost

I am trying to get a route working that will function as a "Thank You" page for people who buy our products on an external store. On localhost everything works fine but on our staging server I get the following exception:
Exception in callback of async function: action#http://taskwunderstaging-45398.onmodulus.net/12289f8cf999b67e6c6c6dcad1a5a5eded53f4e2.js:517:468
Does anyone have an idea what might be causing this?
The code in question is as follows:
The Iron Router Endpoint
Router.route('/signup-partner', {
name: 'signupPartner',
where: 'client',
waitOn: function(){
return Meteor.subscribe("packages");
},
action: function() {
Meteor.logout(function() {});
var query = this.params.query;
//#TODO verify the query with the sha key!
var userInfo = {
email:query.email,
firstname:query.firstname,
lastname:query.lastname,
};
var companyInfo = {
companyName:query.company,
street:query.street,
city:query.city,
zipcode:query.zipcode,
country:query.country,
taxId:query.taxid
};
var orderInfo = {
product:query.product,
order:query.order,
};
// get the package from the database
orderInfo.package = Packages.findOne({digistoreId:orderInfo.product}).name;
orderInfo.tw_id = Packages.findOne({digistoreId:orderInfo.product})._id;
var data = {
userInfo:userInfo,
companyInfo:companyInfo,
orderInfo:orderInfo,
};
var that = this;
// check if the user account already exists and if so add the package and login the user
Meteor.call("partnerUserExists", data.userInfo.email,{orderId:data.orderInfo.order,tw_id:data.orderInfo.tw_id}, function(error, result){
if(result === "not-found"){
that.render('signup_partner',{
data: function(){
return data;
}
});
}
else {
Session.set('boughtPackage',result);
that.redirect('login');
}
});
}
});
the method that this route calls is as follows:
partnerUserExists: function(email,orderIds){
var user = Meteor.users.findOne({"emails.address":email}) || false;
console.log(user);
if(!user){
return "not-found";
}
if(_.indexOf(user.data.digistoreOrders,orderIds.orderId) > -1){
return orderIds.tw_id;
}
(function(callback){
// add the paidTask array if it doesnt exist
if (!user.data.paidTasks){
Meteor.users.update({_id:user._id},{$set:{"data.paidTasks":[]}});
}
// add the digistore array if it doesnt exist
if (!user.data.digistoreOrders){
Meteor.users.update({_id:user._id},{$set:{"data.digistoreOrders":[]}});
}
callback();
})(function(){
Meteor.users.update({_id:user._id},{$push:{"data.digistoreOrders":orderIds.orderId}});
Meteor.users.update({_id:user._id},{$push:{"data.paidTasks":orderIds.tw_id}});
return orderIds.tw_id;
});
}
check for error in your meteor.call. It should tell you if there is an error and why. If not, then try putting a console.log before each return. Overall, I see a lot of user.xx fields being accessed without checking whether that field is set. It could be one of those.

Categories