So I am working on an Express (with Mongoose and Passport) app and I want to include facebook as an authentication method. I have already done it and it works but I don't think I have a proper User model to handle the authentication process for multiple social providers. I want to merge the different social accounts. Here is my current user model which works for facebook auth:
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
facebookId: String,
facebookToken: String
}, { timestamps: true });
I think of the following two approaches but I am not sure if they are viable and which will be the most flexible and independent from the social provider.
I am thinking of having local and social arrays like this:
let userSchema = mongoose.Schema({
local: {
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
},
facebook: {
id: String,
token: String,
email: String,
name: String
},
google: {
id: String,
token: String,
email: String,
}
}, { timestamps: true });
The third approach is just overwriting the social provider id (I am not sure if this is okay).
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
id: String,
token: String,
}, { timestamps: true });
So I found a working solution for myself which might help other people with the same problem. In my User model I have my usual fields and for each social provider I have a separate array like so (users/User.js):
let userSchema = mongoose.Schema({
email: { type: String, unique: true },
name: { type: String },
password: { type: String },
roles: [String],
confirmation_code: String,
confirmed: { type: Boolean, default: false },
facebook: {
id: String,
token: String,
email: String,
name: String
},
google: {
id: String,
token: String,
email: String,
name: String
}
}, { timestamps: true });
When authenticating with a social provider I make an extra check if a user with the same email already exists. If it doesn't, I create a new user. If it does I just add the social provider data (id, token, etc.) to the already existing users array like so (config/passport.js):
passport.use(new FacebookStrategy({
clientID: oauth.facebook.clientID,
clientSecret: oauth.facebook.clientSecret,
callbackURL: oauth.facebook.callbackURL,
profileFields: ['id', 'emails', 'name']
},
function (accessToken, refreshToken, profile, done) {
process.nextTick(function () {
User.findOne({
$or: [
{ 'facebook.id': profile.id },
{ 'email': profile.emails[0].value }
]
}, function (err, user) {
if (err) {
return done(err);
}
if (user) {
if (user.facebook.id == undefined) {
user.facebook.id = profile.id;
user.facebook.token = accessToken;
user.facebook.email = profile.emails[0].value;
user.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
user.save();
}
return done(null, user);
} else {
let newUser = new User();
newUser.facebook.id = profile.id;
newUser.facebook.token = accessToken;
newUser.facebook.email = profile.emails[0].value;
newUser.facebook.name = profile.name.givenName + ' ' + profile.name.familyName;
newUser.name = profile.name.givenName + ' ' + profile.name.familyName;
newUser.email = profile.emails[0].value;
newUser.save(err => {
if (err) {
console.log(err);
throw err;
}
return done(null, newUser);
});
}
});
});
}
));
With this approach you can connect one profile with multiple social providers. However there is one downside. If the user registers a new profile for the first time through a social provider, he won't have a password because social providers don't give back password data (duh). He just needs to change (set) his password through his profile afterwards.
Related
I´m rather new to this..
If I dont want the user to be able to add duplicated countries to visitedCountry, shoulden unique true work?
Or are there any easy way to block that in the patch?
const User = mongoose.model('User', {
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
accessToken: {
type: String,
default: () => crypto.randomBytes(128).toString('hex')
},
visitedCountries:[ {
country: {
type: Object,
ref: "Country",
unique: true
},
comments: String
}]
})
app.patch('/countries', authenticateUser)
app.patch('/countries', async (req, res) => {
const { username, visitedCountry } = req.body
try {
const countryByAlphaCode = await Country.findOne({ alphaCode: visitedCountry }).lean()
const updatedUser = await User.findOneAndUpdate({ username: username, }, {
$push: {
visitedCountries: { country: countryByAlphaCode, comments: "no comments yet"}
},
}, { new: true })
res.json({ success: true, updatedUser })
} catch (error) {
res.status(400).json({ success: false, message: "Invalid request", error })
}
})
The options unique works for all documents. It prevents two (or more) documents from having the same value for your indexed field. It's often used for the email or username.
For your case, I recommend you to perform a check on the user data before you call findOneAndUpdate.
I am trying to save an object in my Mongo database. The issue I face is that when I create the schema, it saves every single entry except the img url. I logged the url before creating the schema and it prints it successfully but when I create the schema object it doesn't get the value from the body.
router.post('/', async (req, res) => {
console.log("URL:", req.body.img) //Logs the url successfully
const pet = new Pet({
name: req.body.name,
petType: req.body.petType,
breed: req.body.breed,
age: req.body.age,
img: req.body.img, //i can't get it here
contact: req.body.contact,
location: req.body.location,
userp: req.body.contact,
})
console.log("This is a Pet");
console.log(pet); //logs everything except the "img" field.
try {
const savedPet = await pet.save();
console.log("This pet was saved", savedPet);
res.json(savedPet); //returns an object without the "img" field
} catch (err) {
res.json({ message: err });
}
});
Edit:
Here is my schema file as well:
const mongoose = require('mongoose');
const petSchema = mongoose.Schema({
name: {
type: String,
required: false
},
petType: {
type: String,
require: true
},
breed: {
type: String,
require: false
},
age: {
type: Number,
require: false
},
img: {
data: String,
require: false
},
contact: {
type: String,
require: true
},
location: {
type: String,
require: true
},
userp: {
type: String,
require: true
}
});
module.exports = mongoose.model('Pet', petSchema);```
I found the error. You use data instead type param specifically in this property.
img: {
type: String,
require: false
},
I'm trying to Query for a user and return the user only if the username and password match what's in the database. Currently if I query with the username, I get the response no matter what, doesn't matter if the password is right or wrong. (Read bottom for context) This is what my graphql schema is:
signInUser: {
type: UserType,
args: {
username: { type: new GraphQLNonNull(GraphQLString) },
password: { type: new GraphQLNonNull(GraphQLString) }
},
resolve(parent, args) {
return User.findOne({ username: args.username }, (err, user) => {
if (bcrypt.compareSync(args.password, user.password)) {
return user;
} else {
return null;
}
});
}
}
This is my UserType schema:
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString },
posts: {
type: new GraphQLList(PostType),
resolve(parent, args) {
// Matches posts based on username
// return _.filter(posts, { name: parent.name });
console.log(parent.name);
return Post.find({ name: parent.name });
}
}
})
});
My MongoDB schema:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
username: String,
password: String
});
module.exports = mongoose.model('User', userSchema);
// model = collection in database
Should I be using JWT? I'm also having trouble wrapping my mind of how to handle knowing that the user has logged in on my client side of the application. I was thinking this way, if this query returned empty I could assume that the login failed, otherwise store the user id into localStorage and use it whenever. Suggestions of other approaches would be greatly appreciated!
I've searched this site for days looking through the many different but similar questions on this topic to no avail.
Here's what I'd like to happen. A user signs in and their posts are automatically linked to the users collection. Eventually I'd like to link posts to the profile it was posted to, but i"m not quite there yet. Here's what I've tried so far.
In the User Schema:
const UserSchema = new Schema({
posts: [{
type: Schema.Types.ObjectId,
ref: 'posts'
}],
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
},
...
});
module.exports = User = mongoose.model('users', UserSchema);
In the Post Schema:
const PostSchema = new Schema({
user: {
type: Schema.Types.ObjectId,
ref: 'users'
},
text: {
type: String,
required: true
},
name: {
type: String
},
...
});
module.exports = Post = mongoose.model('posts', PostSchema);
In my users api, here's how I'm signing the user in and attempting to populate the user's posts:
const User = require('../../models/User');
router.post('/login', (req, res) => {
const { errors, isValid } = validateLoginInput(req.body);
// Check Validation
if (! isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
// Find user by email
User.findOne({ email })
.populate('posts')
.then(user => {
if (! user) {
errors.email = 'User not found';
return res.status(400).json(errors);
}
// Check password
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
// User Matched
// Create JWT Payload
const payload = {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
name: user.firstName + ' ' + user.lastName,
avatar: user.avatar,
posts: user.posts
};
jwt.sign(
payload,
keys.secretOrKey,
{ expiresIn: 3600 }, (err, token) => {
res.json({
success: true,
token: 'Bearer ' + token,
payload
});
});
} else {
errors.password = 'Password is incorrect';
return res.status(400).json(errors);
}
});
});
});
In the posts api, here's how the post is being submitted:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validatePostInput(req.body);
if (! isValid) {
// Return errors with 400 status
return res.status(400).json(errors)
}
const newPost = new Post({
text: req.body.text,
name: req.body.name,
avatar: req.body.avatar,
user: req.user.id
});
newPost.save().then(post => res.json(post));
});
Currently, all I'm seeing is an empty array and no errors. I've been spinning my wheels on this one for a couple days now so any help would be appreciated. Thanks!
I think you forgot to save the _id of your new post to the User model so that the populate() can lookup the posts to populate:
newPost.save().then(post => {
User.update({ _id: req.user.id }, { $push: { posts: post._id }}, (err) => {
res.json(post));
});
});
im getting error: User is not a constructor when trying to add new document to my database. before I used mongoose.model without the Schema method and it worked great but I had to add validator and it needs this syntax and since then I can't make it work
CODE:
var UserSchema = mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, index: true, unique: true, required: true },
password: { type: String, required: true }
});
UserSchema.plugin(uniqueValidator);
let User = mongoose.model("user", UserSchema);
module.exports = User;
router.post('/', (req, res) => {
var user = new User({
username: req.body.username,
email: req.body.email,
password: req.body.password
});
// save the user
user.save(function (err) {
if (err) {
console.log('Error in Saving user: ' + err);
throw err;
}
console.log('User Registration succesful');
// return done(null, userData);
res.status(200).send({user: user})
});
});
mongoose.Schema is a constructor, so you need to call it with "new":
var UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, index: true, unique: true, required: true },
password: { type: String, required: true }
});
ok I fixed my issue instead of
module.exports = User;
I had to do:
module.exports = {User};