Функция забытого пароля сегодня обычно встречается в большинстве веб-приложений. В этом уроке мы узнаем, как реализовать это как на внешнем, так и на внутреннем интерфейсе.

Предпосылки:

Чтобы продолжить изучение этой статьи, вы должны быть знакомы со следующим:

  1. JavaScript
  2. узлы
  3. монгодб

Вот структура проекта:

my-project/
  |-node_modules/
  |-controllers/
  |  |-userController.js
  |-models/
  |   |-userModel.js
  |-routes/
  |   |-userRoute.js
  |-views/
      |-forgot-password.ejs
      |-login.ejs
      |-reset-pasword.ejs
      |-signup.ejs
  |-.env
  |-app.js
  |-package.json
  |-package-lock.json

Требования:
1. Почтовый аккаунт. Вам в основном понадобятся две вещи оттуда: доменное имя песочницы и частный API-КЛЮЧ.

API_KEY='YOUR API KEY'
DOMAIN='sandboxXX.mailgun.org'

2. у нас должны быть установлены следующие пакеты в папке проекта

bcryptjs ---> for securing user password
ejs ---> a templating engine that allows users generate HTML with plain javascript
express ---> nodejs framework
mongoose ---> Object Data Modeling library for MongoDB
nodemailer ---> Node. js module that allows you to send emails from your server with ease
nodemailer-mailgun-tranport ---> allow you to send emails using nodemailer, using the Mailgun API

Файл .env должен содержать следующее:

DOMAIN='Your sandbox domain from mailgun'
API_KEY='Your private API Key'
EMAIL_FROM='The person sending the mail'
MONGO_URI='Mongodb URI string'

давайте создадим наш файл app.js, который будет содержать следующее содержимое:

// app.js
const express = require('express');
const mongoose = require('mongoose');
const userRouter = require('./routes/userRoute');
const view = require('./routes/view');
const port = 5000;
const app = express();
require('dotenv').config();

mongoose.set('strictQuery', false);
//connect to mongodb
const connectDB = async () => {
  try {
    // mongodb connection string
    const con = await mongoose.connect(process.env.MONGO_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log(`mongoDB connected: ${con.connection.host}`);
  } catch (err) {
    console.log(err);
    process.exit(1);
  }
};
connectDB();

// parse incoming json request
app.use(express.json());

app.set('view engine', 'ejs');

// load route
app.use('/api', userRouter);
app.use('/', view);

app.get('/', (req, res) => {
  res.send('Hello');
});

app.listen(port, () => {
  console.log(`server is running on http://localhost:${port}`);
});

Здесь мы создаем схему пользователя и создаем метод createPasswordResetToken, который генерирует токен с помощью встроенного модуля nodejs, который crypto (for encrypting data) мы можем использовать для сброса пароля пользователя.

// userModel.js
const crypto = require('crypto');
const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please tell us your name!'],
  },
  email: {
    type: String,
    required: [true, 'Please provide your email'],
    unique: true,
  },
  password: {
    type: String,
    required: [true, 'Please provide a password'],
  },
  passwordChangedAt: Date,
  passwordResetToken: String,
  passwordResetExpires: Date,
});

userSchema.pre('save', async function (next) {
  // Hash the user password
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

userSchema.methods.createPasswordResetToken = function () {
  // generate random token
  const resetToken = crypto.randomBytes(32).toString('hex');
  // encrypt the token
  this.passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex');
  // sets the time the reset password token expire (10 mins)
  this.passwordResetExpires = Date.now() + 10 * 60 * 1000;
  return resetToken;
};

const User = mongoose.model('User', userSchema);
module.exports = User;

Здесь мы создаем пользовательский маршрут

// userRoute.js
const express = require('express');
const userController = require('../controllers/userController');

const router = express.Router();

router.post('/signup', userController.signup);
router.post('/login', userController.login);
router.post('/forgot-password', userController.forgotPassword);
router.patch('/reset-password/:token', userController.resetPassword);

module.exports = router;

Вот файл представления, в котором мы отображаем страницы внешнего интерфейса.

// view.js
const crypto = require('crypto');
const express = require('express');
const User = require('../models/userModel');
const router = express.Router();

router.get('/signup', (req, res) => {
  res.render('signup');
});
router.get('/login', (req, res) => {
  res.render('login');
});
router.get('/forgot-password', (req, res) => {
  res.render('forgot-password');
});
router.get('/api/reset-password/:token', async (req, res) => {
  const hashedToken = crypto.createHash('sha256').update(req.params.token).digest('hex');

// find the encrypted token and ensures the time hasn't expire
  const user = await User.findOne({
    passwordResetToken: hashedToken,
    passwordResetExpires: { $gt: Date.now() },
  });

  if (!user) {
    return res.redirect('/forgot-password');
  }
  res.render('reset-password', { token: req.params.token });
});

module.exports = router;

теперь в файле пользовательского контроллера мы создаем API, который пользователь может использовать для регистрации, входа и сброса пароля пользователя.

// userController.js
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const mailGun = require('nodemailer-mailgun-transport');
const nodemailer = require('nodemailer');

const User = require('../models/userModel');

exports.signup = async (req, res) => {
  const { name, email, password } = req.body;
  try {
    // creates a user and saves it to the database
    const user = await User.create({ name, email, password });
    res.status(201).send({ user });
  } catch (error) {
    res.status(400).send(error);
  }
};

exports.login = async (req, res) => {
  const { email, password } = req.body;
  try {
    // checks if email exists in the database
    const user = await User.findOne({ email });

    if (!user) {
      return res.status(400).send('Incorrect email or password');
    }

    // checks if user password is correct with the one saved in the database
    const passwordCorrect = await bcrypt.compare(password, user.password);
    if (!passwordCorrect) {
      return res.status(400).send('Incorrect email or password');
    }

    // send response
    res.status(200).json({
      status: 'success',
      user,
    });
  } catch (error) {
    res.status.send(error);
  }
};

exports.forgotPassword = async (req, res) => {
  // find the user, if present in the database
  const user = await User.findOne({ email: req.body.email });

  if (!user) {
    return res.status(400).send('There is no user with that email');
  }

  // Generate the reset token
  const resetToken = user.createPasswordResetToken();
  await user.save();
  const resetUrl = `${req.protocol}://${req.get('host')}/reset-password/${resetToken}`;

  try {
    const message = `Forgot your password? Submit this link: ${resetUrl}.\n If you did not request this, please ignore this email and your password will remain unchanged.`;

    // Step 1
    const auth = {
      auth: {
        api_key: process.env.API_KEY,
        domain: process.env.DOMAIN, //'sandboxXX.'
      },
    };

    // Step 2
    let transporter = nodemailer.createTransport(mailGun(auth));

    // Step 3
    let mailOptions = {
      from: process.env.EMAIL_FROM, // email sender
      to: req.body.email, // email receiver
      subject: 'Reset Your password',
      text: message,
    };

    // Step 4
    const mail = await transporter.sendMail(mailOptions);

    if (!mail) {
      return console.log('There is an error');
    }

    res.status(200).json({
      status: 'success',
      message: 'messsage sent to mail',
    });
  } catch (error) {
    user.passwordResetToken = undefined;
    user.passwordResetExpires = undefined;
    await user.save();
    res.send(error);
  }
};

exports.resetPassword = async (req, res) => {
  try {
     
    const hashedToken = crypto.createHash('sha256').update(req.params.token).digest('hex');
    // Finds user based on the token
    const user = await User.findOne({ passwordResetToken: hashedToken, passwordResetExpires: { $gt: Date.now() } });

    if (!user) {
      return res.status(400).send('Token is invalid or has expired');
    }

    user.password = req.body.password;
    user.passwordResetToken = undefined;
    user.passwordResetExpires = undefined;
    await user.save();
    res.status(200).json({
      status: 'success',
    });
  } catch (error) {
    res.send(error);
  }
}

Используя postman для проверки конечной точки, мы успешно сбросили пароль пользователя.

Note: Since we used mailgun sandbox domain, which is for testing purpose. We can only send email to authorized recipients. On your mailgun account you can set the authorized email that will receive the mail.You can read more here

Теперь бэкенд завершен, мы можем сосредоточиться на фронтенде.

давайте перейдем в нашу папку представлений, которая будет содержать файлы нашего интерфейса.

В файле signup.ejs мы отправляем запрос на регистрацию пользователя.

<!-- Signup.ejs  -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>

    <form>
      <h2>Signup</h2>
      <label for="name">Name</label>
      <input type="text" name="name" required /><br />
      <label for="email">Email</label>
      <input type="text" name="email" required /><br />
      <label for="password">Password</label>
      <input type="password" name="password" required /> <br />

      <button>Sign up</button>
    </form>

    <script>
      const form = document.querySelector('form');

      form.addEventListener('submit', async (e) => {
        e.preventDefault();
        const name = form.name.value;
        const email = form.email.value;
        const password = form.password.value;

        try {
          // fetches data from the signup api
          const res = await fetch('/api/signup', {
            method: 'POST',
            body: JSON.stringify({ name, email, password }),
            headers: { 'Content-Type': 'application/json' },
          });
          const data = await res.json();
          console.log(data);
          if (data.user) {
            alert('sign up successful!');
          }
        } catch (error) {
          console.log(error);
        }
      });
    </script>
  </body>
</html>

страница авторизации:

<!-- Login.ejs -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Login</title>
  </head>
  <body>
    <form action="/api/login">
      <h2>Login</h2>
      <label for="email">Email</label>
      <input type="text" name="email" required /> <br />
      <label for="password">Password</label>
      <input type="password" name="password" required /> <br />
      <button>login</button>
    </form>

    <script>
      const form = document.querySelector('form');

      form.addEventListener('submit', async (e) => {
        e.preventDefault();
        // get form values
        const email = form.email.value;
        const password = form.password.value;

        try {
         // fetches the data from the login api
          const res = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify({ email, password }),
            headers: { 'Content-Type': 'application/json' },
          });
          const data = await res.json();
          if (data.user) {
            alert('logged in');
            location.assign('/');
          }
        } catch (err) {
          console.log(err);
        }
      });
    </script>
  </body>
</html>

Страница с забытым паролем:

<!-- forgot-password.ejs -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <form>
      <div>
        <h2>
          Forgot your password? no problem, just enter the email address that you signed up with to reset your password
        </h2>
        <input type="email" name="email" placeholder="Your Email" inputmode="email" required />
        <button type="submit">Submit</button>
      </div>
    </form>

    <script>
      const form = document.querySelector('form');

      form.addEventListener('submit', async (e) => {
        e.preventDefault();
        // get values
        const email = form.email.value;
        try {
        // fetches data from the forgot-password api     
          const res = await fetch('/api/forgot-password', {
            method: 'POST',
            body: JSON.stringify({ email }),
            headers: { 'Content-Type': 'application/json' },
          });
          const data = await res.json();
          console.log(data);
          if (data.status) {
            alert('link sent to your email, pls copy and paste the link to your browser and set yout password ');
          }
        } catch (err) {
          console.log(err);
        }
      });
    </script>
  </body>
</html>

Вот страница сброса пароля, где мы отправляем запрос к API сброса пароля, чтобы мы могли ввести новый пароль.

<!-- reset-password.ejs -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <form>
      <div>
        <h2>Enter your new password</h2>
        <input type="password" name="password" placeholder="Your Password" required />
        <button type="submit">Submit</button>
      </div>
    </form>
  </body>
  <script>
    const form = document.querySelector('form');

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      // get values
      const password = form.password.value;

      try {
      // fetches the data from the reset-password api
        const res = await fetch('/api/reset-password/<%=token %>', {
          method: 'PATCH',
          body: JSON.stringify({ password }),
          headers: { 'Content-Type': 'application/json' },
        });
        const data = await res.json();
        console.log(data);
        if (data.status) {
          alert('password successfully changed');
          location.assign('/');
        }
      } catch (err) {
        console.log(err);
      }
    });
  </script>
</html>

когда мы запускаем запрос в браузере, мы можем успешно сбросить пароль пользователя.

Так что это будет все для этого урока. До следующего раза… Полный код можно получить здесь.

Рекомендации:

  1. узловой почтовик
  2. почтальон