简体   繁体   中英

How to allow user login by either username or email using node.js

I have a node.js login system with passport but I am trying to figure out how to log in a user with either their username or email. I am only able to log in the user with email or username seperately. I don't know how to write the code to cater for both their username and email. So if a user wants to login with username, they can or if the wish to use their email, they also can. Here is my localstrategy code in my users.js file:

passport.use(new LocalStrategy(
    function(email, password, done) {
        User.getUserByEmail(email, function(err, user, next){
            if(err) throw err;
            if(!user){
                return done(null, false, {message: 'Unknown user'});
            }

        User.comparePassword(password, user.password, function(err, isMatch){
            if(err) throw err;
            if(isMatch){
                return done(null, user);
            } else {
                return done(null, false, {message: 'Invalid password'});
            }
        });
        });
    }));

And here's my module.exports in my user.js:

module.exports.getUserByEmail = function(email, callback){
    var query = {email: email};
    User.findOne(query, callback);
}

module.exports.getUserById = function(id, callback){
    User.findById(id, callback);
}

module.exports.comparePassword = function(candidatePassword, hash, callback){
    bcrypt.compare(candidatePassword, hash, function(err, isMatch) {
        if(err) throw err;
        callback(null, isMatch);
    });
}

The above code only allows user to login with their email. I want users to have the opportunity to login with either their email or username.

You can use getUserById inside the callback of getUserByEmail so that both of the queries run for either email or username . If you specify email , then emailOrUserName will have the email value and then User.getUserByEmail will return the user and next it will proceed with comparePassword .

And if you have username then emailOrUserName will have the username value and then User.getUserByEmail will not return the user so, it executes User.getUserById and if the user is found there then it will proceed with User.comparePassword else it will return Unknown user

passport.use(new LocalStrategy(
    function(emailOrUserName, password, done) {
        User.getUserByEmail(emailOrUserName, function(err, user, next){
            if(err) throw err;
            if(!user){
              User.getUserById(emailOrUserName, function(err, user, next){
                   if(err) throw err;
                   if(!user){
                      return done(null, false, {message: 'Unknown user'});
                   }
            }

        User.comparePassword(password, user.password, function(err, isMatch){
            if(err) throw err;
            if(isMatch){
                return done(null, user);
            } else {
                return done(null, false, {message: 'Invalid password'});
            }
        });
        });
    }));

Expect both username and password within your authentication middleware and then proceed with whatever value you have found as conditions to find the user.

Middleware example:

function authenticateUser(req, res, done) {

  let username = req.body.username,
      password = req.body.password,
      email    = req.body.email;
  let conditions = !!username ? {username: username} : {email: email};

  UserModel.findOne(conditions, (err, user) => {
    if (err)   return done(err);
    if (!user) return done(new Error('Incorrect username or email'));

    return user.comparePassword(password, user.password)
    .then(match => {
      if (match) return done();
      else       return done(new Error('Incorrect password'));
    })
    .catch(error => {
      if (error) return done(new Error(`Unable to validated password. - ${error}`));
    });
  });

}

Now, a front-end developer — with the right documentation — can now actually use either the username , email or both (you will need a bit of JavaScript for both) when building login forms using your endpoint. Here is an example of using both:

HTML:

<form id="login-form" method="POST" action="/login">
  <input id="username-or-email" type="text" placeholder="Username or Email" required/>
  <input type="password" name="password" placeholder="Password"  required/>
  <input type="submit"/>
</form>

JavaScript:

// select the form element
let loginForm = document.querySelector('#login-form');

// add a form handler on submit
loginForm.addEventListener("submit", formHandler);

// validate and the set name attribute as appropriate
function formHandler() {
  /** W3C Email regex: (RFC5322) */
  const email_regex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
  /** Must starts with a letter then can include underscores (_) & hyphens (-) */
  const username_regex = /^[a-zA-Z][\w-]+$/;

  let input = document.querySelector('#username-or-email');

  if (email_regex.test(input.value)) {
    // it is an email, send the value as an email
    input.setAttribute("name", "email");
  } else if (username_regex.test(input.value)) {
    // it is a username, send the value as a username
    input.setAttribute("name", "username");
  } else {
    // invalid email or username format, return an error message to user
  }

}

So you get to validate and set your dynamic input at the same time. Keep in mind that the regular expression should match your username and email data model as much as possible.

Do this in local strategy

function(username, password, done) {
    var criteria = (username.indexOf('@') === -1) ? {username: username} : {email: username};
    User.findOne(criteria, function (err, user) { //implementation }

Just got into this situation, and I though I'll share my final code to verify user using either username or email :

userSchema.statics.findByCredentials = async credentials => {
    const {
        email = undefined,
        username = undefined,
        password = undefined
    } = credentials

    if ((!!email && !!username) || (!email && !username)) {
        throw new Error('Should provide either email or username.')
    }

    if (!password) {
        throw new Error('Password is required.')
    }

    const user = await User.findOne(email ? { email } : { username })
    if (!user) {
        throw new Error('Credentials are invalid!')
    }

    if (!bcrypt.compare(password, user.password)) {
        throw new Error('Credentials are invalid!')
    }

    return user
}

So I am using this function to verify if the user provided valid credentials, and from my handler, I call the function on the model class.

I add little more on the above answer shared by 
user7153178: Mine is related to the backend, using 
context.

inside you Login component in the frontend try to 
have these

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");

then you will be needing this function on your 
form:

const handleSubmit = () => {
const user = {
  email,
  password,
};

if (email === "" || password === "") {
  setError("Please fill in your credentials");
} else {
  loginUser(user, context.dispatch);
}
};

this is one of the input as part of your form
<Input
placeholder={"Email or Phone Number"}
name={"email"}
id={"email"}
value={email}
onChangeText={(text) => 
setEmail(text.toLowerCase())}
 />

In the backend write your post like that:

router.post("/login", async (req, res) => {
const user = await User.findOne(
{ $or: [{ email: req.body.email }, { phone: 
req.body.email }] }
);

if (!user) {
return res.status(400).send("The user not found");
}

if (user && bcrypt.compareSync(req.body.password, 
user.passwordHash)) {
const token = jwt.sign(
  {
    userId: user.id,
    isAdmin: user.isAdmin,
  },
  secret,
  { expiresIn: "1d" }
);

res.status(200).send({ user: user.email, token: 
token });
} else {
res.status(400).send("password is wrong!");
}

});

The code below can be used to allow your users to login using either their username/password

 if (!req.body.username && !req.body.email) {
    res.status(400).send({ message: "All input is required, username/email is missing", status: false });
    return;
  }
  if (!req.body.password) {
    res.status(400).send({ message: "All input is required, password is missing", status: false });
    return;
  }
  
    let whereClause = {};
    if (req.body.email) {
      whereClause.email = req.body.email;
    } else if (req.body.username) {
      whereClause.username = req.body.username.toLowerCase();
    }
    const user = await User.findOne({ where: whereClause });
      // If user not found
      if (!user) {
        return res.status(404).send({ message: "User not found", status: false });
      }

You can wrap it in a try-catch block for proper error handling

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM