0

I'm making a route for the user registration and I need to use multiple queries so that I could determine if the username and email exist in the database.

My first solution was to create a connection.query inside connection.query yet it doesn't work.

const { username, email, password, confirm } = req.body
var error_mssg = []
connection.query(
  `SELECT COUNT(*) as usernameCount FROM adminuser WHERE username="${username}"`,
  (err, data) => {
    if (err) throw err;

    if(data[0].usernameCount){
      error_mssg.push("username already exist")
    }

    connection.query(
      `SELECT COUNT(*) as emailCount FROM adminuser WHERE email="${email}"`,
      (err, data) => {
        if (err) throw err;

        if (data[0].emailCount > 0){
          error_mssg.push("email aready exist")
        }
  }
);

I also tried adding multipleStatemts: true inside my mysql.createConnection which works fine but I remember that I still need to make another connection.query if no same username/email found in the database and if password == confirm.

connection.query(
  `SELECT COUNT(*) as usernameCount FROM adminuser WHERE username="${username}";
  SELECT COUNT(*) as emailCount FROM adminuser WHERE email="${email}"`,
  (err, data) => {
    if (err) throw err;

    if(data[0][0].usernameCount){
      error_mssg.push("username already exist")
    }

    if(data[1][0].emailCount){
      error_mssg.push("email already exist")
    }

    if(password === confirm){
      if(error_mssg.lenght > 0){
        *******query to insert user information*******
      }else{
        res.send(error_mssg)
      }

    }else{
      error_mssg.push("password does not match")
      res.send(error_mssg)
    }
  }
);

How can I use multiple queries and then insert the data if error_mssg is empty.

Nellartsa
  • 29
  • 1
  • 7
  • "It doesn't work" isn't terribly descriptive. A problem statement should state exactly how something isn't behaving as expected, including any error messages, output or other results, as well as the desired/expected results. See the help center for more info on [asking good questions](https://stackoverflow.com/help/how-to-ask). – outis Apr 27 '21 at 04:52

2 Answers2

2

Injection

First, close the injection vulnerability. User input should never be trusted, and never passed directly to another subsystem. The best thing to do is pass user data in a side-channel; in SQL, this means using prepared queries. For example, instead of connection.query, you could use connection.execute:

connection.execute(
    'SELECT Count(*) AS usernameCount FROM adminuser WHERE username=?',
    [username],
    (err, data) => {...}
)

The available MySQL driver may instead only support escaping arguments and simulate prepared statements, but that's better than nothing (though if that's the case, you should seriously consider switching to one that supports prepared statements):

connection.query(
    mysql.format('SELECT Count(*) AS usernameCount FROM adminuser WHERE username=?', [username]), 
    ...

Logic Error

Take a closer look at the block in the multiple statement example:

if(password === confirm){
  if(error_mssg.lenght > 0){
    *******query to insert user information*******
  }else{
    res.send(error_mssg)
  }
...

Note that the user information is inserted if there are error messages, and the (empty) error message list is returned if not. This is the opposite of what you want. Debuggers can help catch this sort of logic error.

Rather than only sending the existence error messages if the passwords do not match, you could instead send all error messages. This way, a user can correct all errors at once, rather than having to submit only to discover other errors. This will also better separate out request validation from request handling.

if (password !== confirm) {
    error_msgs.push("passwords do not match");
}

if (error_msgs.length) {
    res.send(error_msgs);
} else {
    // create new user
    ...
}

Existence Testing

Another approach to querying for existing records is to use a single query to check for existing usernames and e-mail addresses in the adminuser database. This has an efficiency advantage over chained queries or awaits because one query doesn't need to wait for the other to finish (though the performance cost of the latter two is likely negligible).

Since the two fields are separate columns in the same database, a query is straightforward:

SELECT username, email FROM adminuser WHERE username=? OR email=?;

The data requirement that usernames and e-mails should be unique should be reflected in the schema by the use of UNIQUE indices. This will limit the result rows to at most 2 (and the max occurrence of each value to 1), which can then be checked for the presence of the unique values.

connection.execute(
    'SELECT username, email FROM adminuser WHERE username=? OR email=?;',
    [username, email],
    (err, data) => {
        if (err) throw err;
        
        /*** Validate user creation request ***/
        /* There should be at most 2 rows, with at most 1 occurrence each of `username` and `email`. */
        for (var iRow=0; iRow < data.length; ++iRow) {
            if (username == data[iRow].username) {
                error_msgs.push("username already exists");
            }
            if (email == data[iRow].email) {
                error_msgs.push("email already exists");
            }
        }
        
        if (password !== confirm) {
            error_msgs.push("passwords do not match");
        }
        
        /*** Handle user creation request ***/
        if (error_msgs.length) {
            res.send(error_msgs);
        } else {
            // create new user
            ...
        }
    }
);

The existence checking can be made more robust (in case something goes wrong with the database and multiple occurrences of a username or email are returned) at the cost of a more complex SQL statement. If supported by the driver, named placeholders (instead of '?') can be used both for clarity and to reduce redundancy.

// namedPlaceholders can also be an `execute` option
connection.config.namedPlaceholders = true
...

connection.execute(
    'SELECT Sum(If(:username=username, 1, 0) As usernameCount, 
            Sum(If(:email=email, 1, 0) AS emailCount 
         FROM adminuser 
         WHERE username=:username OR email=:email;',
    { username, email }, // you could instead pass req.body, assuming it's been checked for 'username' and 'email' properties
    (err, data) => {
        if (err) throw err;
        
        /*** Validate user creation request ***/
        if (data[0].usernameCount) {
            error_msgs.push("username already exists");
        }
        if (data[0].emailCount) {
            error_msgs.push("email already exists");
        }
        if (password !== confirm) {
            error_msgs.push("passwords do not match");
        }
        
        /*** Handle user creation request ***/
        if (error_msgs.length) {
            res.send(error_msgs);
        } else {
            // create new user
            ...
        }
    });

Additional design considerations

The sample code mixes a couple of different concerns: database access and request handling (validation &c.). Put another way, the sample code bears multiple responsibilities, which is undesirable. A code unit should have as few responsibilities as possible; a class should have a single responsibility. The most prevalent definition of single responsibility is the Single Responsibility Principle: "a class should have only one reason to change". This definition doesn't exactly apply to code units larger than classes (depending on how the reasons to change are expressed).

In the sample, consider maintenance tasks that might arise:

  • adding/switching DBMS support
  • updating user field validation (including adding fields)
  • sending messages (to e.g. admins, mods, the new user) when a user is created

Each of these is a different reason for code to change, and thus a different responsibility.

Separating concerns will result in classes with reduced responsibilities. One module, the Data Access Layer, should handle managing users in the database (e.g. checking for username or email in existing records, adding new users), and another should handle the user creation request (validation & user creation) by making the appropriate calls to the DAL (perhaps indirectly). In particular, SQL statements and database calls shouldn't appear in the request handler. The request handler might look something like:

async function validateUserCreationRequest(req) {
    const { username, email, password, confirm } = req.body;
    var error_msgs = [],
        userExists = await User.exists({ username, email }); // here, User is responsible for accessing the DAL

    if (userExists) {
        if (userExists.usernameCount) {
            error_msgs.push("username already exists");
        }
        if (userExists.emailCount) {
            error_msgs.push("email already exists");
        }
    }
    
    if (password !== confirm) {
        error_msgs.push("passwords do not match");
    }
    
    if (error_msgs.length) {
        return error_msgs;
    }
    return null;
}

function createUser(req) {
    return User.create(req); // again, User is responsible for accessing the DAL
}

function handleUserCreationRequest(req) {
    var errors = validateUserCreationRequest(req);
    if (errors) {
        res.send(errors);
    } else {
        createUser(req);
    }
}

When code bears multiple responsibilities, it's generally highly coupled (there's a high degree of interdependence between code that handles different tasks), which makes maintenance more difficult and results in more bugs. Separating those responsibilities reduces coupling and increases cohesion (the code in modules belong together, in that they're narrowly focused on what tasks they accomplish), as coupling and cohesion are inversely related.

There's a fair amount of overlap between concerns and responsibilities, but they are distinct principles.

There can be situations when these principles can be violated, but it should arise from the software requirements and be a conscious decision to do so.

outis
  • 75,655
  • 22
  • 151
  • 221
  • That's the most detailed explanation I've read. I didn't notice that I mistakenly miswrote my logic here but I already rewrote my logic the same way you made it. I also didn't think that I could validate the `username/email` like that so that's awesome. Last question why can't I write the `query` inside the `request`? I don't really get what `coupling` is. – Nellartsa Apr 27 '21 at 05:43
  • @Nellartsa: see update, and check the linked pages for more detail. – outis Sep 10 '21 at 03:11
  • What a fantastic response. – Long_dog Jan 16 '23 at 22:36
1

You can try with async queries. it is better approach to get same as you trying with multiple statement in same query.

const usernameCount = await db.query( `SELECT COUNT(*) as usernameCount FROM adminuser WHERE username="${username}"` );
const emailCount = await db.query( `SELECT COUNT(*) as emailCount FROM adminuser WHERE email="${email}"` );
Ashish Sharma
  • 446
  • 5
  • 14
  • I have a `const Register = async (req, res) => { ... }` and tried making a variable with `await` but when I tried to `console.log(Register)` it returns ` Query { ...long things I dont understand }`. Are you using a different library? I'm using `mysql` – Nellartsa Apr 24 '21 at 16:37
  • please post your code snippet with bit more detail so I can help you in better way. – Ashish Sharma Apr 25 '21 at 06:35
  • Thank to your answer I found `mysql2` library which can supports `async await` and it fixed my problem. – Nellartsa Apr 27 '21 at 05:24