О чем эта статья?

Если вы думали о создании какой-либо информационной панели, вы, вероятно, понимаете, что вам необходимо реализовать аутентификацию.
Вы, вероятно, уже знакомы с такими терминами, как «Вход» и «Регистрация». Вероятно, вы использовали их для регистрации на dev.to 😅

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

В этой статье вы узнаете, как создать приложение, использующее двухфакторную аутентификацию с помощью React, Novu и Node.js.

Безопасность


Что такое двухфакторная аутентификация (2FA)?

Двухфакторная аутентификация, иногда называемая двухфакторной аутентификацией, представляет собой дополнительную меру безопасности, которая позволяет пользователям подтвердить свою личность перед получением доступа к учетной записи.
Это может быть реализовано с помощью аппаратного токена, текстовых SMS-сообщений, push-уведомлений и биометрических данных, необходимых приложению, прежде чем пользователи смогут авторизоваться для выполнения различных действий.

В этой статье мы будем использовать 2FA для обмена текстовыми сообщениями SMS, создав приложение, которое принимает учетные данные пользователей и проверяет их личность, прежде чем предоставить им доступ к приложению.


Novu — первая инфраструктура уведомлений с открытым исходным кодом

Просто краткая справка о нас. Novu — первая платформа с открытым исходным кодом. инфраструктура уведомлений. Мы в основном помогаем управлять всеми уведомлениями о продуктах. Может быть В приложении (значок колокольчика, как у вас в сообществе разработчиков — Веб-сокеты), электронные письма, SMS-сообщения и так далее.

Буду очень признателен, если вы поможете нам, отметив библиотеку 🤩

Гитхаб


Настройка сервера Node.js

Создайте папку проекта, содержащую две подпапки с именами client и server.

mkdir auth-system
cd auth-system
mkdir client server
Войти в полноэкранный режим

Выйти из полноэкранного режима

Перейдите в server папку и создайте файл package.json.

cd server & npm init -y
Войти в полноэкранный режим

Выйти из полноэкранного режима

Установите Express.js, CORS и Nodemon.

npm install express cors nodemon
Войти в полноэкранный режим

Выйти из полноэкранного режима

Express.js — это быстрый минималистичный фреймворк, предоставляющий несколько функций для создания веб-приложений в Node.js. КОРС — это пакет Node.js, который обеспечивает связь между разными доменами. Нодемон — это инструмент Node.js, который автоматически перезапускает сервер после обнаружения изменений в файле.

Создать index.js файл — точка входа на веб-сервер.

touch index.js
Войти в полноэкранный режим

Выйти из полноэкранного режима

Настройте простой сервер Node.js, как показано ниже:

const express = require("express");
const cors = require("cors");
const app = express();
const PORT = 4000;

app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());

app.get("/api", (req, res) => {
    res.json({ message: "Hello world" });
});

app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});
Войти в полноэкранный режим

Выйти из полноэкранного режима


Создание пользовательского интерфейса приложения

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

Создайте новый проект React.js в папке клиента.

cd client
npx create-react-app ./
Войти в полноэкранный режим

Выйти из полноэкранного режима

Удалите лишние файлы, такие как логотип и тестовые файлы, из приложения React и обновите App.js файл для отображения Hello World как показано ниже.

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}
export default App;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Перейдите в src/index.css файл и скопируйте приведенный ниже код. Он содержит весь CSS, необходимый для стилизации этого проекта.

@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    font-family: "Space Grotesk", sans-serif;
}
input {
    height: 45px;
    padding: 10px 15px;
    margin-bottom: 15px;
}
button {
    width: 200px;
    outline: none;
    border: none;
    padding: 15px;
    cursor: pointer;
    font-size: 16px;
}
.login__container,
.signup__container,
.verify,
.dashboard {
    width: 100%;
    min-height: 100vh;
    padding: 50px 70px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}
.login__form,
.verify__form,
.signup__form {
    width: 100%;
    display: flex;
    flex-direction: column;
}
.loginBtn,
.signupBtn,
.codeBtn {
    background-color: green;
    color: #fff;
    margin-bottom: 15px;
}
.signOutBtn {
    background-color: #c21010;
    color: #fff;
}
.link {
    cursor: pointer;
    color: rgb(39, 147, 39);
}
.code {
    width: 50%;
    text-align: center;
}
.verify__form {
    align-items: center;
}

@media screen and (max-width: 800px) {
    .login__container,
    .signup__container,
    .verify {
        padding: 30px;
    }
}
Войти в полноэкранный режим

Выйти из полноэкранного режима

Установить Реактивный маршрутизатор — библиотека JavaScript, которая позволяет нам перемещаться между страницами в приложении React.

npm install react-router-dom
Войти в полноэкранный режим

Выйти из полноэкранного режима

Создайте папку компонентов в приложении React, содержащую Signup.js, Login.js, PhoneVerify.js а также Dashboard.js файлы.

mkdir components
cd components
touch Signup.js Login.js PhoneVerify.js Dashboard.js
Войти в полноэкранный режим

Выйти из полноэкранного режима

Обновите App.js файл для рендеринга вновь созданных компонентов на разных маршрутах через React Router.

import { BrowserRouter, Route, Routes } from "react-router-dom";
import Login from "./components/Login";
import Signup from "./components/Signup";
import Dashboard from "./components/Dashboard";
import PhoneVerify from "./components/PhoneVerify";

function App() {
    return (
        <BrowserRouter>
            <Routes>
                <Route path=" element={<Login />} />
                <Route path='/register' element={<Signup />} />
                <Route path='/dashboard' element={<Dashboard />} />
                <Route path='phone/verify' element={<PhoneVerify />} />
            </Routes>
        </BrowserRouter>
    );
}

export default App;
Войти в полноэкранный режим

Выйти из полноэкранного режима


Страница входа

Скопируйте приведенный ниже код в Login.js файл. Он принимает адрес электронной почты и пароль от пользователя.

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const Login = () => {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ email, password });
        setPassword("");
        setEmail("");
    };

    const gotoSignUpPage = () => navigate("/register");

    return (
        <div className='login__container'>
            <h2>Login </h2>
            <form className='login__form' onSubmit={handleSubmit}>
                <label htmlFor='email'>Email</label>
                <input
                    type='text'
                    id='email'
                    name='email'
                    value={email}
                    required
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='password'>Password</label>
                <input
                    type='password'
                    name='password'
                    id='password'
                    minLength={8}
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <button className='loginBtn'>SIGN IN</button>
                <p>
                    Don't have an account?{" "}
                    <span className='link' onClick={gotoSignUpPage}>
                        Sign up
                    </span>
                </p>
            </form>
        </div>
    );
};

export default Login;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Авторизоваться


Страница регистрации

Скопируйте приведенный ниже код в Signup.js файл. Он принимает электронную почту, имя пользователя, телефон и пароль от пользователя.

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const Signup = () => {
    const [email, setEmail] = useState("");
    const [username, setUsername] = useState("");
    const [tel, setTel] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ email, username, tel, password });
        setEmail("");
        setTel("");
        setUsername("");
        setPassword("");
    };
    const gotoLoginPage = () => navigate("/");

    return (
        <div className='signup__container'>
            <h2>Sign up </h2>
            <form className='signup__form' onSubmit={handleSubmit}>
                <label htmlFor='email'>Email Address</label>
                <input
                    type='email'
                    name='email'
                    id='email'
                    value={email}
                    required
                    onChange={(e) => setEmail(e.target.value)}
                />
                <label htmlFor='username'>Username</label>
                <input
                    type='text'
                    id='username'
                    name='username'
                    value={username}
                    required
                    onChange={(e) => setUsername(e.target.value)}
                />
                <label htmlFor='tel'>Phone Number</label>
                <input
                    type='tel'
                    name='tel'
                    id='tel'
                    value={tel}
                    required
                    onChange={(e) => setTel(e.target.value)}
                />
                <label htmlFor='tel'>Password</label>
                <input
                    type='password'
                    name='password'
                    id='password'
                    minLength={8}
                    required
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <button className='signupBtn'>SIGN UP</button>
                <p>
                    Already have an account?{" "}
                    <span className='link' onClick={gotoLoginPage}>
                        Login
                    </span>
                </p>
            </form>
        </div>
    );
};

export default Signup;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вздох


Страница PhoneVerify

Обновите PhoneVerify.js файл, содержащий приведенный ниже код. Он принимает проверочный код, отправленный на номер телефона пользователя.

import React, { useState } from "react";
import { useNavigate } from "react-router-dom";

const PhoneVerify = () => {
    const [code, setCode] = useState("");
    const navigate = useNavigate();

    const handleSubmit = (e) => {
        e.preventDefault();
        console.log({ code });
        setCode("");
        navigate("/dashboard");
    };
    return (
        <div className='verify'>
            <h2 style={{ marginBottom: "30px" }}>Verify your Phone number</h2>
            <form className='verify__form' onSubmit={handleSubmit}>
                <label htmlFor='code' style={{ marginBottom: "10px" }}>
                    A code has been sent your phone
                </label>
                <input
                    type='text'
                    name='code'
                    id='code'
                    className='code'
                    value={code}
                    onChange={(e) => setCode(e.target.value)}
                    required
                />
                <button className='codeBtn'>AUTHENTICATE</button>
            </form>
        </div>
    );
};

export default PhoneVerify;
Войти в полноэкранный режим

Выйти из полноэкранного режима

Приборная доска


Страница панели инструментов

Скопируйте приведенный ниже код в Dashboard.js файл. Это защищенная страница, доступная только для аутентифицированных пользователей.

import React, {useState} from "react";
import { useNavigate } from "react-router-dom";

const Dashboard = () => {
    const navigate = useNavigate();

useEffect(() => {
        const checkUser = () => {
            if (!localStorage.getItem("username")) {
                navigate("/");
            }
        };
        checkUser();
    }, [navigate]);

    const handleSignOut = () => {
        localStorage.removeItem("username");
        navigate("/");
    };

    return (
        <div className='dashboard'>
            <h2 style={{ marginBottom: "30px" }}>Howdy, David</h2>
            <button className='signOutBtn' onClick={handleSignOut}>
                SIGN OUT
            </button>
        </div>
    );
};

export default Dashboard;
Войти в полноэкранный режим

Выйти из полноэкранного режима


Создание рабочего процесса аутентификации

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


Маршрут регистрации

Создайте функцию внутри Signup компонент, который отправляет учетные данные пользователя на сервер Node.js.

const postSignUpDetails = () => {
    fetch("http://localhost:4000/api/register", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
            tel,
            username,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            console.log(data);
        })
        .catch((err) => console.error(err));
};

const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Call it within the submit function
    postSignUpDetails();
    setEmail("");
    setTel("");
    setUsername("");
    setPassword("");
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Создайте маршрут POST внутри index.js файл на сервере, который принимает учетные данные пользователя.

app.post("/api/register", (req, res) => {
    const { email, password, tel, username } = req.body;
    //👇🏻 Logs the credentials to the console
    console.log({ email, password, tel, username });
})
Войти в полноэкранный режим

Выйти из полноэкранного режима

Поскольку нам нужно сохранить учетные данные пользователя, обновите маршрут POST, как показано ниже:

//👇🏻 An array containing all the users
const users = [];

//👇🏻 Generates a random string as the ID
const generateID = () => Math.random().toString(36).substring(2, 10);

app.post("/api/register", (req, res) => {
    //👇🏻 Get the user's credentials
    const { email, password, tel, username } = req.body;

    //👇🏻 Checks if there is an existing user with the same email or password
    let result = users.filter((user) => user.email === email || user.tel === tel);

    //👇🏻 if none
    if (result.length === 0) {
        //👇🏻 creates the structure for the user
        const newUser = { id: generateID(), email, password, username, tel };
        //👇🏻 Adds the user to the array of users
        users.push(newUser);
        //👇🏻 Returns a message
        return res.json({
            message: "Account created successfully!",
        });
    }
    //👇🏻 Runs if a user exists
    res.json({
        error_message: "User already exists",
    });
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Обновите postSignUpDetails функционировать в рамках Signup компонент для уведомления пользователей об успешной регистрации.

const postSignUpDetails = () => {
    fetch("http://localhost:4000/api/register", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
            tel,
            username,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                alert(data.message);
                navigate("/");
            }
        })
        .catch((err) => console.error(err));
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

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


Маршрут входа

Создайте функцию с компонентом Login, которая отправляет адрес электронной почты и пароль пользователя на сервер.

const postLoginDetails = () => {
    fetch("http://localhost:4000/api/login", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            console.log(data);
        })
        .catch((err) => console.error(err));
};
const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Calls the function
    postLoginDetails();
    setPassword("");
    setEmail("");
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Создайте маршрут POST на сервере, который аутентифицирует пользователя.

app.post("/api/login", (req, res) => {
    //👇🏻 Accepts the user's credentials
    const { email, password } = req.body;
    //👇🏻 Checks for user(s) with the same email and password
    let result = users.filter(
        (user) => user.email === email && user.password === password
    );

    //👇🏻 If no user exists, it returns an error message
    if (result.length !== 1) {
        return res.json({
            error_message: "Incorrect credentials",
        });
    }
    //👇🏻 Returns the username of the user after a successful login
    res.json({
        message: "Login successfully",
        data: {
            username: result[0].username,
        },
    });
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Обновите postLoginDetails для отображения ответа от сервера.

const postLoginDetails = () => {
    fetch("http://localhost:4000/api/login", {
        method: "POST",
        body: JSON.stringify({
            email,
            password,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                //👇🏻 Logs the username to the console
                console.log(data.data);
                //👇🏻 save the username to the local storage
                localStorage.setItem("username", data.data.username);
                //👇🏻 Navigates to the 2FA route
                navigate("/phone/verify");
            }
        })
        .catch((err) => console.error(err));
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Приведенный выше фрагмент кода отображает сообщение об ошибке для пользователя в случае возникновения ошибки; в противном случае имя пользователя, полученное с сервера, сохраняется в локальном хранилище для упрощения идентификации.

В следующих разделах я расскажу вам, как добавить двухфакторную аутентификацию по SMS с помощью Novu.


Как добавить Novu в приложение Node.js

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

Чтобы установить Novu Node.js SDK, запустите приведенный ниже фрагмент кода на своем сервере.

npm install @novu/node
Войти в полноэкранный режим

Выйти из полноэкранного режима

Создайте проект Novu, запустив приведенный ниже код. Вам доступна персонализированная панель управления.

npx novu init
Войти в полноэкранный режим

Выйти из полноэкранного режима

Вам нужно будет войти в Github перед созданием проекта Novu. Фрагмент кода ниже содержит шаги, которые вы должны выполнить после запуска npx novu init

Now let's setup your account and send your first notification
❓ What is your application name? Devto Clone
❓ Now lets setup your environment. How would you like to proceed?
   > Create a free cloud account (Recommended)
❓ Create your account with:
   > Sign-in with GitHub
❓ I accept the Terms and Condidtions ( and have read the Privacy Policy (
    > Yes
✔️ Create your account successfully.

We've created a demo web page for you to see novu notifications in action.
Visit:  to continue
Войти в полноэкранный режим

Выйти из полноэкранного режима

Посетите демонстрационную веб-страницу , скопируйте свой идентификатор подписчика со страницы и нажмите кнопку «Пропустить учебник». Мы будем использовать его позже в этом уроке.

Новый 1

Скопируйте свой ключ API, доступный в разделе «Настройки» в разделе «Ключи API» на Платформа управления Novu.

2 ноября

Импортируйте Novu из пакета и создайте экземпляр, используя свой ключ API на сервере.

//👇🏻 Within server/index.js

const { Novu } = require("@novu/node");
const novu = new Novu("<YOUR_API_KEY>");
Войти в полноэкранный режим

Выйти из полноэкранного режима


Добавление двухфакторной SMS-аутентификации с Novu

Novu поддерживает несколько инструментов для обмена текстовыми SMS-сообщениями, таких как Twilio, Nexmo, Plivo, Amazon SNS и многие другие. В этом разделе вы узнаете, как добавить SMS-сообщения Twilio в Novu.


Настройка учетной записи Twilio

Перейти к Домашняя страница Twilio и создайте учетную запись. Вам нужно будет подтвердить свой адрес электронной почты и номер телефона.

Отправляйтесь к своему Консоль Twilio как только вы войдете в систему.

Создайте номер телефона Twilio на панели инструментов. Этот номер телефона является виртуальным номером, который позволяет вам общаться через Twilio.

Скопируйте номер телефона Twilio куда-нибудь на свой компьютер; для использования позже в учебнике.

Прокрутите вниз до раздела «Информация об учетной записи», скопируйте и вставьте SID учетной записи и токен аутентификации куда-нибудь на свой компьютер. (будет использоваться позже в этом уроке).


Подключение Twilio SMS к Novu

Выберите Integrations Store на боковой панели панели инструментов Novu и прокрутите вниз до раздела SMS.

Выберите Twilio и введите необходимые учетные данные, предоставленные Twilio, затем нажмите «Подключиться».

2 ноября

Затем создайте шаблон уведомления, выбрав «Уведомления» на боковой панели.

4 ноября

Выберите Редактор рабочих процессов на боковой панели и создайте рабочий процесс, как показано ниже:

5 ноября

Щелкните SMS из рабочего процесса и добавьте текст ниже в поле содержимого сообщения.

Your verification code is {{code}}
Войти в полноэкранный режим

Выйти из полноэкранного режима

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

Вернитесь к index.js файл на сервере и создайте функцию, которая отправляет SMS для проверки пользователей при входе в приложение. Добавьте приведенный ниже код в index.js файл:

//👇🏻 Generates the code to be sent via SMS
const generateCode = () => Math.random().toString(36).substring(2, 12);

const sendNovuNotification = async (recipient, verificationCode) => {
    try {
        let response = await novu.trigger("<NOTIFICATION_TEMPLATE_ID>", {
            to: {
                subscriberId: recipient,
                phone: recipient,
            },
            payload: {
                code: verificationCode,
            },
        });
        console.log(response);
    } catch (err) {
        console.error(err);
    }
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Фрагмент кода выше принимает номер телефона получателя и код подтверждения в качестве параметра.

Обновите login Маршрут POST для отправки SMS через Novu после входа пользователя в приложение.

//👇🏻 variable that holds the generated code
let code;

app.post("/api/login", (req, res) => {
    const { email, password } = req.body;

    let result = users.filter(
        (user) => user.email === email && user.password === password
    );

    if (result.length !== 1) {
        return res.json({
            error_message: "Incorrect credentials",
        });
    }
    code = generateCode();

    //👇🏻 Send the SMS via Novu
    sendNovuNotification(result[0].tel, code);

    res.json({
        message: "Login successfully",
        data: {
            username: result[0].username,
        },
    });
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Чтобы проверить код, введенный пользователем, обновите PhoneVerify компонент для отправки кода на сервер.

const postVerification = async () => {
    fetch("http://localhost:4000/api/verification", {
        method: "POST",
        body: JSON.stringify({
            code,
        }),
        headers: {
            "Content-Type": "application/json",
        },
    })
        .then((res) => res.json())
        .then((data) => {
            if (data.error_message) {
                alert(data.error_message);
            } else {
                //👇🏻 Navigates to the dashboard page
                navigate("/dashboard");
            }
        })
        .catch((err) => console.error(err));
};
const handleSubmit = (e) => {
    e.preventDefault();
    //👇🏻 Calls the function
    postVerification();
    setCode("");
};
Войти в полноэкранный режим

Выйти из полноэкранного режима

Создайте маршрут POST на сервере, который принимает код и проверяет, совпадает ли он с кодом на бэкэнде.

app.post("/api/verification", (req, res) => {
    if (code === req.body.code) {
        return res.json({ message: "You're verified successfully" });
    }
    res.json({
        error_message: "Incorrect credentials",
    });
});
Войти в полноэкранный режим

Выйти из полноэкранного режима

Поздравляем! 🎊 Вы завершили проект для этого урока.


Вывод

Итак, вы узнали, что такое двухфакторная аутентификация, ее различные формы и как добавить ее в веб-приложение.

Двухфакторная аутентификация добавляется в программные приложения для защиты как учетных данных пользователя, так и ресурсов, к которым пользователи могут получить доступ. В зависимости от вашего приложения вы можете добавить 2FA в определенные части, где происходят значительные изменения.

Исходный код этого приложения доступен здесь:

Спасибо за чтение!

PS Буду очень признателен, если вы поможете нам, отметив библиотеку 🤩

Гитхаб