Иди, иди, иди! Это неделя Capstone в моей школе, Galvanize Austin, двухнедельный проект, в котором мы используем все навыки, полученные за последние шесть месяцев, для создания полнофункционального приложения!

Для моего Capstone я решил создать приложение под названием Smiles. Smiles — это мобильная платформа, которая помогает доставлять остатки еды в приюты для бездомных. Для моего мобильного интерфейса я использую React и React-native в дополнение к Axios, чтобы получить доступ к моему абстрактному API. До этого проекта единственным фреймворком, который я использовал, был Angular, но услышать о React и возможностях React-native для создания приложений как для iOS, так и для Android, казалось очень заманчивым. Мне очень нравится использовать React, она не похожа ни на одну другую библиотеку, которую я использовал раньше (не забудьте привязать эту!!!)

Для моего API я использую Express и Knex для запроса моей базы данных PostgreSQL, а также Bcrypt для авторизации. Все это размещается на сервере Heroku, чтобы мой интерфейс мог получить к нему доступ.

В моем приложении есть три разных типа пользователей: продавцы еды, волонтеры и приюты. Однако все они начинаются на странице входа и после этого разветвляются.

Страница входа принимает адрес электронной почты и пароль и отправляет запрос Axios POST с учетными данными при нажатии кнопки входа. Оттуда мой API должен найти пользователя и отправить обратно его данные или «неверный логин». Сложность заключается в том, что в базе данных есть таблица для каждого типа пользователей, и, имея только адрес электронной почты и пароль, я не могу узнать, в какой таблице они находятся.

Первой моей мыслью было, что я буду проверять все таблицы одну за другой, пока не найду пользователя или не дойду до конца, а пользователя нет. Хотя это сработало бы, очень быстро стало трудно читать и казалось, что код жестко запрограммирован. Я знал, что должно быть лучшее решение.

Следующей моей мыслью было то, что процесс проверки каждой таблицы был идентичен, поэтому я мог бы использовать итератор для уменьшения объема кода, вот мое решение.

const titles = ['foodvendors','volunteers','shelters'];
router.post('/login', (req, res) => {
  titles.forEach(title => {
    knex(title)
    .where({
      email: req.body.email
    })
    .first()
    .then(user => {
      if(user) {
        bcrypt.compare(req.body.password, user.hashedPassword)
        .then((valid) => {
          if (valid) {
            user.title = title;
            res.send(user);
          }
        });
      }
    });
  });
  res.send('invalid login');
});

Хотя это значительно уменьшило объем кода, что сделало его легко читаемым, в нем был недостаток. В то время как forEach является синхронным, запросы knex являются асинхронными, поэтому forEach отправляет асинхронный запрос для каждой таблицы. До того, как какой-либо из ответов вернулся, forEach завершается, и сразу же отправляется «неверный логин». Неееет!! Так близко!

Эта проблема заставила меня почесать голову, мне нужен был какой-то способ убедиться, что я получил ответы на все запросы knex до того, как смогу отправить «неверный логин». Мое решение состояло в том, чтобы использовать счетчик, который будет подсчитываться после возврата асинхронного вызова knex, таким образом, как только подсчет достигнет длины массива таблиц, я знал, что все они вернулись!

const titles = ['foodvendors','volunteers','shelters'];
router.post('/login', (req, res) => {
  let completed = 0;
  titles.forEach(title => {
    knex(title)
    .where({
      email: req.body.email
    })
    .first()
    .then(user => {
      if(user) {
        bcrypt.compare(req.body.password, user.hashedPassword)
        .then((valid) => {
          if (valid) {
            user.title = title;
            res.send(user);
          }
          completed++;
          if(completed === titles.length) {
            res.send('invalid login')
          }
        });
      }
      else {
        completed++;
        if(completed === titles.length) {
          res.send('invalid login')
        }
      }
    });
  });
});

Успех! Это сработало, к сожалению, мне понадобилось две проверки счетчика, одна на обнаружение пользователя, а другая на отсутствие. Однако я по-прежнему стремился найти лучшее решение.

Затем я обратился к Javascript Promises. Обещание — это возможное завершение асинхронной операции. Для моей проблемы Promise.all — идеальное решение! Promise.all принимает массив промисов и имеет метод .then, который возвращает все результирующие значения только после того, как все промисы выполнены. Для этого я использовал карту, чтобы превратить мой массив таблиц для запроса в массив промисов, запрашивающих указанные таблицы. Оттуда я вызываю Promise.all.

const titles = ['foodvendors','volunteers','shelters'];
router.post('/login', (req, res) => {
  let queries = titles.map(title => {
    return knex(title)
    .where({
      email: req.body.email
    })
    .first()
  });
  Promise.all(queries)
  .then((results) => {
    results.forEach(tableResult => {
      if(tableResult){
        res.send(tableResult);
      }
    });
  });
  res.send('invalid login');
});

Хотя это решение работало, мне по-прежнему требовался способ реализации моей авторизации с помощью сравнения Bcrypt. Проблема заключалась в том, что Bcrypt также является асинхронной операцией. Мне нужно было бы связать другой Promise.all, чтобы после возврата запросов knex Bcrypt мог выполняться, а ответ «неверный вход» ждал, пока не вернутся результаты Bcrypt. Мне казалось, что добавление еще одного Promise.all сделает код трудным для чтения.

Моя следующая мысль заключалась в том, что если бы я создал новую таблицу в своей базе данных, называемую электронной почтой, содержащую электронные письма и соответствующие им заголовки, то я мог бы сначала проверить таблицу электронной почты, чтобы найти таблицу, в которой находится пользователь, а затем я мог бы перейти прямо к этой таблице. и найти пользователя. Если электронная почта не находится в таблице электронной почты, я сразу узнаю, что электронной почты нет ни в одной таблице.

Это решение, хотя и добавляет в базу данных дополнительную таблицу, обеспечивает решение не только этой проблемы, но и многих других. Например, при добавлении нового поставщика продуктов питания я не смогу использовать адрес электронной почты, используемый в другой таблице. Эта таблица электронной почты поможет сделать эту проверку намного проще. Вот мое выполнение этого решения:

router.post('/login', (req, res) => {
  knex('emails')
  .where({
    email: req.body.email
  })
  .first()
  .then(account => {
    if(account) {
      console.log(account.title);
      knex(account.title)
      .where({
        email: req.body.email
      })
      .first()
      .then(user => {
        console.log(user);
        bcrypt.compare(req.body.password, user.hashedPassword)
        .then((valid) => {
          if (valid) {
            user.title = account.title;
            res.send(user);
          }
        });
      });
    } else {
      res.send('invalid login');
    }
  });
});

Отлично, все работает, и я могу войти в свое приложение под любым пользователем!