Большинство из нас хотя бы раз сталкивались с процессом восстановления учетной записи — когда мы забываем пароль, возникает необходимость в процедуре создания нового и восстановления доступа к системе. В этой статье основное внимание уделяется реализации такого процесса с использованием Node.js, Knex и некоторых нераскрытых инструментов наряду с Express для обработки маршрутов и выполнения необходимых операций.
Мы рассмотрим реализацию маршрутизатора, обработку параметров URL-адреса, определение того, что отправлять пользователю, когда в качестве доказательства доступен только адрес электронной почты или номер телефона, управление отправкой электронной почты и решение проблем безопасности.
Прежде чем углубиться в программирование, я хотел бы убедиться, что мы работаем с той же кодовой базой, к которой вы можете получить доступ из моего публичного доступа.
Теперь взгляните на схему процесса забытого пароля.
Сервер будет отвечать за отправку электронных писем в почтовый ящик пользователя, содержащих действительную ссылку для сброса пароля, а также будет проверять наличие токена и пользователя.
Чтобы начать использовать службу электронной почты и отправлять электронные письма с помощью Node.js, нам необходимо установить следующие пакеты в дополнение к нашим существующим зависимостям:
npm i --save nodemailer handlebars
Nodemailer : Мощный модуль, который позволяет легко отправлять электронные письма с использованием SMTP или других транспортных механизмов.
Handlebars : Handlebars — популярный шаблонизатор для JavaScript. Это позволит нам определять шаблоны с заполнителями, которые можно будет заполнить данными при рендеринге.
Теперь нам нужно создать миграцию, поэтому в моем случае мне нужно добавить новый столбец forgot_password_token
в таблицу users
:
knex migrate:make add_field_forgot_password_token -x ts
и в сгенерированном файле я установил код:
import type { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.string('forgot_password_token').unique(); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.alterTable('users', table => { table.dropColumn('forgot_password_token'); }); }
Миграция токена забытого пароля в таблице пользователей
а затем перенесите последний файл:
knex migrate:knex
Итак, теперь мы можем установить в таблице users
наш forgot_password_token
Для управления контроллерами, ответственными за обработку логики забывания и сброса пароля, мы должны установить два маршрута. Первый маршрут инициирует процесс забытого пароля, а второй обрабатывает процесс сброса, ожидая параметра токена в URL-адресе для проверки. Чтобы реализовать это, создайте файл с именем forgotPasswordRouter.ts
в каталоге src/routes/
и вставьте следующий код:
import { Router } from 'express'; import { forgotPasswordController } from 'src/controllers/forgotPasswordController'; import { resetPasswordController } from 'src/controllers/resetPasswordController'; export const forgotPasswordRouter = Router(); forgotPasswordRouter.post('/', forgotPasswordController); forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Забыли пароль Маршрутизатор
Два контроллера будут управлять логикой отправки электронных писем и сброса пароля.
Когда клиент забывает свой пароль, у него нет сеанса, а это означает, что мы не можем получить данные пользователя, кроме электронной почты или любых других идентификаторов безопасности. В нашем случае мы отправляем электронное письмо для сброса пароля. Эту логику мы собираемся установить в контроллер.
forgotPasswordRouter.post('/', forgotPasswordController);
Помните «забыли пароль?» ссылка под формой входа обычно в пользовательском интерфейсе любого клиента в форме входа? Нажав на нее, мы попадем в окно, где мы можем запросить сброс пароля. Мы просто вводим адрес электронной почты, и контроллер выполняет все необходимые процедуры. Давайте рассмотрим следующий код:
import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; import { TokenService } from 'src/services/TokenService'; import { EmailService } from 'src/services/EmailService'; export const forgotPasswordController = async (req: Request, res: Response) => { try { const { email, }: { email: string; } = req.body; const user = await UserModel.findByEmail(email); if (user) { const token = await TokenService.sign( { id: user.id, }, { expiresIn: '1 day', } ); await user.context.update({ forgot_password_token: token }); await EmailService.sendPasswordResetEmail(email, token); } return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };
Контроллер забытого пароля
Из тела мы получим электронное письмо, а затем найдем пользователя с помощью UserModel.findByEmail
. Если пользователь существует, мы создаем токен JWT с помощью TokenService.sign
и сохраняем токен для пользователя forgot_password_token
со сроком действия 1 день. Затем мы отправим сообщение на электронную почту с соответствующей ссылкой и токеном, с помощью которого пользователь сможет изменить свой пароль.
Чтобы иметь возможность отправлять электронное письмо, нам необходимо создать новый адрес электронной почты, который будет отправителем.
Перейдем в Google, чтобы создать новую учетную запись электронной почты, а затем, когда учетная запись будет создана, перейдем к ссылке «Управление учетной записью Google» . Найти его можно в правом верхнем углу, нажав на аватар. Затем в левом меню нажмите на пункт «Безопасность» , а затем нажмите «2-этапная проверка» . Ниже вы найдете раздел «Пароли приложений» , нажмите на стрелочку:
Введите имя, которое необходимо использовать. В моем случае я устанавливаю Nodemailer
и нажимаю Create .
Скопируйте сгенерированный пароль и установите его в свой файл .env
. Нам нужно установить в файл две переменные:
MAIL_USER="[email protected]" MAIL_PASSWORD="vyew hzek avty iwst"
Конечно, чтобы иметь правильный адрес электронной почты, например info@company_name.com
, вам необходимо настроить Google Workspace или AWS Amazon WorkMail вместе с AWS SES или любыми другими сервисами. Но в нашем случае мы бесплатно используем простую учетную запись Gmail.
Подготовив файл .env
, мы готовы настроить наш сервис для отправки электронных писем. Контроллер будет использовать службу с сгенерированным токеном и адресом электронной почты получателя для нашего сообщения.
await EmailService.sendPasswordResetEmail(email, token);
Давайте создадим src/services/EmailService.ts
и определим класс для службы:
export class EmailService {}
И теперь в качестве исходных данных мне нужно получить среду для использования с nodemailer
:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; }
Служба электронной почты
Нам нужно позаботиться об инициализации сервиса. Я уже писал об этом в предыдущем
import { TokenService } from 'src/services/TokenService'; import { RedisService } from 'src/services/RedisService'; import { EmailService } from 'src/services/EmailService'; export const initialize = async () => { await RedisService.initialize(); TokenService.initialize(); EmailService.initialize(); };
Инициализация служб
Теперь приступим к созданию инициализации в нашем классе EmailService
:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; dotenv.config(); export class EmailService { private static transporter: nodemailer.Transporter; private static env = { USER: process.env.MAIL_USER, PASS: process.env.MAIL_PASSWORD, }; public static initialize() { try { EmailService.transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: this.env.USER, pass: this.env.PASS, }, }); } catch (error) { console.error('Error initializing email service'); throw error; } } }
Инициализация службы электронной почты
Существует инициализация nodemailer.createTransport()
, метод, предоставляемый библиотекой nodemailer
. Он создает объект-транспортер, который будет использоваться для отправки наших электронных писем. Метод принимает объект параметров в качестве аргумента, в котором вы указываете детали конфигурации транспортера.
Мы используем Google: service: 'gmail'
указывает поставщика услуг электронной почты. Nodemailer обеспечивает встроенную поддержку различных поставщиков услуг электронной почты, а gmail
указывает, что транспортер будет настроен для работы с SMTP-сервером Gmail.
Для аутентификации auth
необходимо установить учетные данные, необходимые для доступа к SMTP-серверу поставщика услуг электронной почты.
Для user
должен быть установлен адрес электронной почты, с которого мы собираемся отправлять электронные письма, и этот пароль был сгенерирован в учетной записи Google из паролей приложений.
Теперь давайте настроим последнюю часть нашего сервиса:
import process from 'process'; import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { generateAttachments } from 'src/helpers/generateAttachments'; import { generateTemplate } from 'src/helpers/generateTemplate'; import { getHost } from 'src/helpers/getHost'; dotenv.config(); export class EmailService { // ...rest code public static async sendPasswordResetEmail(email: string, token: string) { try { const host = getHost(); const template = generateTemplate<{ token: string; host: string; }>('passwordResetTemplate', { token, host }); const attachments = generateAttachments([{ name: 'email_logo' }]); const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, }); console.log('Message sent: %s', info.messageId); } catch (error) { console.error('Error sending email: ', error); } } }
Отправить электронное письмо для сброса пароля
Прежде чем продолжить, очень важно определить подходящий хост, на котором клиент получит электронное письмо. Очень важно установить ссылку с токеном в теле письма.
import * as dotenv from 'dotenv'; import process from 'process'; dotenv.config(); export const getHost = (): string => { const isProduction = process.env.NODE_ENV === 'production'; const protocol = isProduction ? 'https' : 'http'; const port = isProduction ? '' : `:${process.env.CLIENT_PORT}`; return `${protocol}://${process.env.WEB_HOST}${port}`; };
Получить хост
В качестве шаблонов я использую handlebars
, и для этого нам нужно создать в src/temlates/passwordResetTemplate.hbs
наш первый HTML-шаблон:
<!-- passwordResetTemplate.hbs --> <html lang='en'> <head> <style> a { color: #372aff; } .token { font-weight: bold; } </style> <title>Forgot Password</title> </head> <body> <p>You requested a password reset. Please use the following link to reset your password:</p> <a class='token' href="{{ host }}/reset-password/{{ token }}">Reset Password</a> <p>If you did not request a password reset, please ignore this email.</p> <img src="cid:email_logo" alt="Email Logo"/> </body> </html>
Шаблон сброса пароля
и теперь мы можем повторно использовать этот шаблон с помощником:
import path from 'path'; import fs from 'fs'; import handlebars from 'handlebars'; export const generateTemplate = <T>(name: string, props: T): string => { const templatePath = path.join(__dirname, '..', 'src/templates', `${name}.hbs`); const templateSource = fs.readFileSync(templatePath, 'utf8'); const template = handlebars.compile(templateSource); return template(props); };
Создать помощник по шаблонам
Чтобы улучшить нашу электронную почту, мы можем даже включать вложения. Для этого добавьте файл email_logo.png
в папку src/assets
. Затем мы можем отобразить это изображение в электронном письме, используя следующую вспомогательную функцию:
import path from 'path'; import { Extension } from 'src/@types/enums'; type AttachmentFile = { name: string; ext?: Extension; cid?: string; }; export const generateAttachments = (files: AttachmentFile[] = []) => files.map(file => { const ext = file.ext || Extension.png; const filename = `${file.name}.${ext}`; const imagePath = path.join(__dirname, '..', 'src/assets', filename); return { filename, path: imagePath, cid: file.cid || file.name, }; });
Помощник по созданию вложений
Собрав всех этих помощников, мы должны иметь возможность отправлять электронную почту, используя:
const info = await EmailService.transporter.sendMail({ from: this.env.USER, to: email, subject: 'Password Reset', html: template, attachments, });
Этот подход обеспечивает достойную масштабируемость, позволяя службе использовать различные методы отправки электронных писем с разнообразным содержанием.
Теперь давайте попробуем запустить контроллер с помощью нашего маршрутизатора и отправить электронное письмо. Для этого я использую
Консоль сообщит вам, что сообщение отправлено:
Message sent: <1k96ah55-c09t-p9k2–[email protected]>
Проверьте наличие новых сообщений во входящих:
Ссылка на сброс пароля должна содержать токен и хост:
http://localhost:3000/reset-password/<token>
Здесь указан порт 3000
, поскольку это сообщение относится к процессу разработки. Это означает, что клиент, ответственный за обработку форм для сброса пароля, также будет работать в среде разработки.
Токен должен быть проверен на стороне контроллера с помощью TokenService, откуда мы можем получить пользователя, отправившего это электронное письмо. Давайте восстановим маршрутизатор, который использует токен:
forgotPasswordRouter.post('/reset/:token', resetPasswordController);
Контроллер обновит пароль только в том случае, если токен действителен и не истек, в соответствии со временем истечения срока действия, установленным в один час. Чтобы реализовать эту функцию, перейдите в папку src/controllers/
и создайте файл с именем resetPasswordController.ts
, содержащий следующий код:
import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { TokenService } from 'src/services/TokenService'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const resetPasswordController = async (req: Request, res: Response) => { try { const token = req.params.token; if (!token) { return res.sendStatus(400); } const userData = await TokenService.verify<{ id: number }>(token); const user = await UserModel.findOneById<User>(userData.id); if (!user) { return res.sendStatus(400); } const newPassword = req.body.password; if (!newPassword) { return res.sendStatus(400); } const hashedPassword = await bcrypt.hash(newPassword, 10); await UserModel.updateById(user.id, { password: hashedPassword, passwordResetToken: null }); return res.sendStatus(200); } catch (error) { const errors = ['jwt malformed', 'TokenExpiredError', 'invalid token']; if (errors.includes(error.message)) { return res.sendStatus(400); } return res.sendStatus(500); } };
Сброс пароля контроллера
Этот контроллер получит токен, проверит его, извлечет идентификатор пользователя из расшифрованных данных, получит соответствующего пользователя, получит новый пароль, отправленный клиентом в теле запроса, и приступит к обновлению пароля в базе данных. В конечном итоге это позволяет клиенту войти в систему, используя новый пароль.
Масштабируемость службы электронной почты демонстрируется с помощью различных подходов, таких как отправка подтверждений или сообщений об успехе, например, тех, которые указывают на обновление пароля и позволяют последующий вход в систему. Однако управление паролями представляет собой сложную задачу, особенно когда необходимо повысить безопасность приложений.
Существует множество вариантов повышения безопасности, включая дополнительные проверки перед разрешением изменения пароля, такие как сравнение токенов, электронная почта и проверка пароля.
Другой вариант — внедрить систему PIN-кодов, при которой код отправляется на электронную почту пользователя для проверки на стороне сервера. Каждая из этих мер требует использования возможностей отправки электронной почты.
Весь реализованный код вы можете найти в файле
Пожалуйста, не стесняйтесь проводить любые эксперименты с этой сборкой и делитесь своими отзывами о том, какие аспекты этой темы вам нравятся. Большое спасибо.
Здесь вы можете найти несколько ссылок, которые я использовал в этой статье:
Также опубликовано здесь