Как реализовать 2FA в Laravel с помощью Inertia.js и Vue 3?

Я работаю над проектом, использующим Laravel, Inertia.js и Vue 3, и хочу реализовать двухфакторную аутентификацию (2FA) для входа в систему пользователя. Моя установка включает базу данных MySQL с таблицей пользователей, которая содержит столбец 2fa_enabled, указывающий, требуется ли 2FA для пользователя.

Вот рабочий процесс, которого я хотел бы достичь:

  1. Когда пользователь входит в систему в первый раз, требуется 2FA, если для 2fa_enabled установлено значение true.
  2. Система отправляет код подтверждения на номер мобильного телефона и электронную почту пользователя.
  3. Пользователь вводит код, который затем проверяется.
  4. Если код верен, система сохраняет код в базе данных и перенаправляет пользователя на личный кабинет.
  5. Если код неверен, пользователь увидит сообщение об ошибке.

Во время реализации я столкнулся с некоторыми проблемами и не знаю, как правильно настроить процесс создания и обработки кода проверки. Я пробовал использовать Firebase для отправки SMS-кодов и проверки ReCaptcha, но столкнулся со следующими проблемами:

Непойманный (в обещании) ReferenceError: Firebase не определена Сбои проверки рекапчи
Обработка проверки кода в моем компоненте Vue 3

Буду признателен за любые рекомендации о том, как:

  • Правильно создавайте и обрабатывайте коды 2FA с помощью Firebase. Управление проверкой ReCaptcha.
  • Обработка проверки кода в моем компоненте Vue 3.

Вот как я настраиваю верификатор ReCaptcha и инициализацию Firebase:

import { initializeApp } from 'firebase/app';
import { getAuth, RecaptchaVerifier } from 'firebase/auth';

const firebaseConfig = {
    apiKey: "YOUR_API_KEY",
    authDomain: "YOUR_AUTH_DOMAIN",
    projectId: "YOUR_PROJECT_ID",
    storageBucket: "YOUR_STORAGE_BUCKET",
    messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
    appId: "YOUR_APP_ID",
    measurementId: "YOUR_MEASUREMENT_ID"
};

const app = initializeApp(firebaseConfig);
const auth = getAuth(app);

const recaptchaContainer = document.getElementById('recaptcha-container');
const recaptchaVerifier = new RecaptchaVerifier(recaptchaContainer, {
    size: 'invisible',
    callback: (response) => {
        console.info('reCAPTCHA resolved');
    },
    'expired-callback': () => {
        console.info('reCAPTCHA expired');
    }
}, auth);

async function getCode(phoneNumber) {
    try {
        const confirmationResult = await auth.signInWithPhoneNumber(phoneNumber, recaptchaVerifier);
        console.info('Code sent:', confirmationResult);
    } catch (error) {
        console.error('Error during sign-in with phone number:', error);
        console.error('Error code:', error.code);
    }
}

// Example usage
getCode('+91112223334');

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
305
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я следую Официальным документам Firebase, поэтому нашел несколько ошибок, теперь все работает нормально.

import { getAuth, RecaptchaVerifier, signInWithPhoneNumber} from "firebase/auth";
initializeApp(firebaseConfig);
const auth = getAuth();
onMounted(() => {
const recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {

в этой строке

<script setup>
import { initializeApp } from 'firebase/app';
import { getAuth, RecaptchaVerifier, signInWithPhoneNumber } from "firebase/auth";
import { computed, ref, onMounted } from 'vue';
import { router } from "@inertiajs/vue3";

const props = defineProps({
    phoneNumber: {
        type: String,
        required: true,
    },
});

const verificationCode = ref('');
let confirmationResult = null;

const otpPart1 = ref('');
const otpPart2 = ref('');
const otpPart3 = ref('');
const otpPart4 = ref('');
const otpPart5 = ref('');
const otpPart6 = ref('');

const firebaseConfig = {
  apiKey: "****",
  authDomain: "****.firebaseapp.com",
  projectId: "****",
  storageBucket: "****.appspot.com",
  messagingSenderId: "****",
  appId: "5:****:web:****",
  measurementId: "****"
};


initializeApp(firebaseConfig);
const auth = getAuth();

onMounted(() => {
    const recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
        size: 'invisible',
        callback: (response) => {
            console.info('reCAPTCHA resolved');
        },
        'expired-callback': () => {
            console.info('reCAPTCHA expired, please resolve reCAPTCHA again.');
        }
    }, auth);
    window.recaptchaVerifier = recaptchaVerifier;
    sendVerificationCode(props.phoneNumber);
});

function sendVerificationCode(phoneNumber) {
    const appVerifier = window.recaptchaVerifier;

    signInWithPhoneNumber(auth, phoneNumber, appVerifier)
        .then((result) => {
            confirmationResult = result;
        })
        .catch((error) => {
            console.error('Error sending verification code:', error);
        });
}

function handleFormSubmit() {
    const otpCode = `${otpPart1.value}${otpPart2.value}${otpPart3.value}${otpPart4.value}${otpPart5.value}${otpPart6.value}`;
    verifyCode(otpCode);
}

function verifyCode(verificationCode) {
    if (confirmationResult) {
        confirmationResult.confirm(verificationCode)
            .then((result) => {
                router.post(route('admin.sendverificationcode'), { 'verificationCode': verificationCode })

            })
            .catch((error) => {
                console.error('Error verifying code:', error);
            });
    } else {
        console.error('Confirmation result not available.');
    }
}

const maskedPhoneNumber = computed(() => {
    const phoneNumberLength = props.phoneNumber.length;
    const digitsToShow = 4;
    const startIndex = phoneNumberLength - digitsToShow;
    const maskedPart = '*'.repeat(startIndex);
    const unmaskedPart = props.phoneNumber.slice(startIndex);
    return maskedPart + unmaskedPart;
});
</script>
<style scoped>
.login-card .card-body {
    padding: 2.5rem !important;
}

.signIn-heading {
    margin-bottom: 20px;
}

.twoStepArea {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 15px;
}

.three-input-groups {
    display: flex;
}

.three-input-groups input[type = "number"] {
    width: 36px;
    height: 49px;
    border-radius: 0;
    border: 1px solid #e2e2e2;
    outline: 0;
    padding: 10px 5px;
    text-align: center;
}

.three-input-groups input:first-child {
    border-top-left-radius: 6px;
    border-bottom-left-radius: 6px;
}

.three-input-groups input[type = "number"]+input[type = "number"] {
    border-left: 0;
}

.three-input-groups input:last-child {
    border-top-right-radius: 6px;
    border-bottom-right-radius: 6px;
}

.twoStep-spacer {
    font-size: 34px;
    font-weight: 700;
    color: #e2e2e2;
    line-height: 1;
    margin-top: -10px;
}
</style>
<template>
<div class = "row justify-content-center">
                    <div class = "col-md-8 col-lg-6 col-xl-5">
                        <div class = "card mt-4 login-card">
                            <div class = "card-body p-4">
                                <div>
                                    <h5 class = "text-primary signIn-heading">Two-step authentication</h5>
                                    <p>Enter the 6-digit verification code sent to <b>your mobile
                                            number {{ maskedPhoneNumber }}.</b></p>
                                </div>
                                <div>
                                    <form @submit.prevent = "handleFormSubmit">
                                        <div id = "recaptcha-container"></div>
                                        <div class = "otp-container mt-4">
                                            <div class = "twoStepArea">
                                                <div class = "three-input-groups">
                                                    <input type = "number" v-model = "otpPart1" maxlength = "1"
                                                        class = "otp-input" />
                                                    <input type = "number" v-model = "otpPart2" maxlength = "1"
                                                        class = "otp-input" />
                                                    <input type = "number" v-model = "otpPart3" maxlength = "1"
                                                        class = "otp-input" />
                                                </div>
                                                <div class = "twoStep-spacer">-</div>
                                                <div class = "three-input-groups">
                                                    <input type = "number" v-model = "otpPart4" maxlength = "1"
                                                        class = "otp-input" />
                                                    <input type = "number" v-model = "otpPart5" maxlength = "1"
                                                        class = "otp-input" />
                                                    <input type = "number" v-model = "otpPart6" maxlength = "1"
                                                        class = "otp-input" />
                                                </div>
                                            </div>
                                        </div>
                                        <div class = "mt-4">
                                            <button class = "btn btn-primary w-100" type = "submit"
                                                >Verify Phone Number</button>
                                        </div>
                                    </form>
                                </div>
                            </div>
                            <!-- end card body -->
                        </div>
                        <!-- end card -->
                    </div>
                </div>
</template>

Другие вопросы по теме