Описание изменений
This commit is contained in:
@@ -1,141 +0,0 @@
|
||||
<template>
|
||||
<div class="telegram-auth">
|
||||
<button v-if="!showVerification" class="auth-btn telegram-btn" @click="startTelegramAuth">
|
||||
<span class="auth-icon">📱</span> Подключить Telegram
|
||||
</button>
|
||||
|
||||
<div v-else class="verification-form">
|
||||
<input
|
||||
type="text"
|
||||
v-model="verificationCode"
|
||||
placeholder="Введите код из Telegram"
|
||||
/>
|
||||
<button class="auth-btn verify-btn" @click="verifyCode">Подтвердить</button>
|
||||
<button class="auth-btn cancel-btn" @click="cancelVerification">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import axios from '../api/axios';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const showVerification = ref(false);
|
||||
const verificationCode = ref('');
|
||||
|
||||
const startTelegramAuth = () => {
|
||||
// Открываем Telegram бота в новом окне
|
||||
window.open('https://t.me/your_bot_username', '_blank');
|
||||
showVerification.value = true;
|
||||
};
|
||||
|
||||
const verifyCode = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/telegram/verify', {
|
||||
code: verificationCode.value
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
auth.setTelegramAuth(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error verifying Telegram code:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelVerification = () => {
|
||||
showVerification.value = false;
|
||||
verificationCode.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.telegram-auth {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.telegram-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.auth-progress {
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check-btn {
|
||||
background-color: #0088cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff4d4f;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-code {
|
||||
font-family: monospace;
|
||||
font-size: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
</style>
|
||||
@@ -1,203 +0,0 @@
|
||||
<template>
|
||||
<div class="wallet-connection">
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-if="!authStore.isAuthenticated">
|
||||
<button @click="connectWallet" class="connect-button" :disabled="loading">
|
||||
<div v-if="loading" class="spinner"></div>
|
||||
{{ loading ? 'Подключение...' : 'Подключить кошелек' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="wallet-info">
|
||||
<span class="address">{{ formatAddress(authStore.user?.address) }}</span>
|
||||
<button @click="disconnectWallet" class="disconnect-btn">Выйти</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { connectWithWallet } from '../utils/wallet';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const isConnecting = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
onWalletAuth: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
isAuthenticated: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
// Форматирование адреса кошелька
|
||||
const formatAddress = (address) => {
|
||||
if (!address) return '';
|
||||
return `${address.substring(0, 6)}...${address.substring(address.length - 4)}`;
|
||||
};
|
||||
|
||||
// Функция для подключения кошелька
|
||||
const connectWallet = async () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
await props.onWalletAuth();
|
||||
} catch (err) {
|
||||
console.error('Ошибка при подключении кошелька:', err);
|
||||
error.value = 'Ошибка подключения кошелька';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Автоматическое подключение при загрузке компонента
|
||||
onMounted(async () => {
|
||||
console.log('WalletConnection mounted, checking auth state...');
|
||||
|
||||
// Проверяем аутентификацию на сервере
|
||||
const authState = await authStore.checkAuth();
|
||||
console.log('Auth state after check:', authState);
|
||||
|
||||
// Если пользователь уже аутентифицирован, не нужно ничего делать
|
||||
if (authState.authenticated) {
|
||||
console.log('User is already authenticated, no need to reconnect');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли сохраненный адрес кошелька
|
||||
const savedAddress = localStorage.getItem('walletAddress');
|
||||
|
||||
if (savedAddress && window.ethereum) {
|
||||
console.log('Found saved wallet address:', savedAddress);
|
||||
|
||||
try {
|
||||
// Проверяем, разблокирован ли MetaMask, но не запрашиваем разрешение
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_accounts' // Используем eth_accounts вместо eth_requestAccounts
|
||||
});
|
||||
|
||||
if (accounts && accounts.length > 0) {
|
||||
console.log('MetaMask is unlocked, connected accounts:', accounts);
|
||||
|
||||
// Если кошелек разблокирован и есть доступные аккаунты, проверяем совпадение адреса
|
||||
if (accounts[0].toLowerCase() === savedAddress.toLowerCase()) {
|
||||
console.log('Current account matches saved address');
|
||||
|
||||
// Не вызываем handleConnectWallet() автоматически,
|
||||
// просто показываем пользователю, что он может подключиться
|
||||
} else {
|
||||
console.log('Current account does not match saved address');
|
||||
localStorage.removeItem('walletAddress');
|
||||
}
|
||||
} else {
|
||||
console.log('MetaMask is locked or no accounts available');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking MetaMask state:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция для отключения кошелька
|
||||
const disconnectWallet = async () => {
|
||||
try {
|
||||
// Сначала отключаем MetaMask
|
||||
if (window.ethereum) {
|
||||
try {
|
||||
// Просто очищаем слушатели событий
|
||||
window.ethereum.removeAllListeners();
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting MetaMask:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Затем выполняем выход из системы
|
||||
await authStore.disconnect(router);
|
||||
} catch (error) {
|
||||
console.error('Error disconnecting wallet:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wallet-connection {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.connect-button {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.connect-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #d32f2f;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: #ffebee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.wallet-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.address {
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background-color: #f44336;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,195 +1,117 @@
|
||||
<template>
|
||||
<div class="email-connect">
|
||||
<p>Подключите свой email для быстрой авторизации.</p>
|
||||
|
||||
<div class="email-form">
|
||||
<div class="email-connection">
|
||||
<div v-if="!showVerification">
|
||||
<input
|
||||
type="email"
|
||||
v-model="email"
|
||||
placeholder="Введите ваш email"
|
||||
:disabled="loading || verificationSent"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="Введите email"
|
||||
class="email-input"
|
||||
/>
|
||||
<button
|
||||
@click="sendVerification"
|
||||
class="connect-button"
|
||||
:disabled="!isValidEmail || loading || verificationSent"
|
||||
@click="requestCode"
|
||||
:disabled="isLoading || !isValidEmail"
|
||||
class="email-btn"
|
||||
>
|
||||
<span class="email-icon">✉️</span> {{ verificationSent ? 'Код отправлен' : 'Отправить код' }}
|
||||
Получить код
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="verificationSent" class="verification-form">
|
||||
<div v-else>
|
||||
<input
|
||||
type="text"
|
||||
v-model="verificationCode"
|
||||
placeholder="Введите код подтверждения"
|
||||
:disabled="loading"
|
||||
v-model="code"
|
||||
type="text"
|
||||
placeholder="Введите код"
|
||||
class="code-input"
|
||||
/>
|
||||
<button
|
||||
@click="verifyEmail"
|
||||
class="verify-button"
|
||||
:disabled="!verificationCode || loading"
|
||||
@click="verifyCode"
|
||||
:disabled="isLoading"
|
||||
class="verify-btn"
|
||||
>
|
||||
Подтвердить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Загрузка...</div>
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="success" class="success">{{ success }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import axios from 'axios';
|
||||
|
||||
const email = ref('');
|
||||
const verificationCode = ref('');
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const verificationSent = ref(false);
|
||||
|
||||
const isValidEmail = computed(() => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email.value);
|
||||
const props = defineProps({
|
||||
onEmailAuth: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
async function sendVerification() {
|
||||
if (!isValidEmail.value) return;
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
// Запрос на отправку кода подтверждения
|
||||
const response = await axios.post('/api/auth/email', {
|
||||
email: email.value
|
||||
}, {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
error.value = `Ошибка: ${response.data.error}`;
|
||||
return;
|
||||
}
|
||||
|
||||
verificationSent.value = true;
|
||||
success.value = `Код подтверждения отправлен на ${email.value}`;
|
||||
} catch (err) {
|
||||
console.error('Error sending verification code:', err);
|
||||
error.value = 'Ошибка при отправке кода подтверждения';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
const email = ref('');
|
||||
const code = ref('');
|
||||
const error = ref('');
|
||||
const isLoading = ref(false);
|
||||
const showVerification = ref(false);
|
||||
|
||||
async function verifyEmail() {
|
||||
if (!verificationCode.value) return;
|
||||
|
||||
const isValidEmail = computed(() => {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
|
||||
});
|
||||
|
||||
const requestCode = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
isLoading.value = true;
|
||||
await props.onEmailAuth(email.value);
|
||||
showVerification.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
// Запрос на проверку кода
|
||||
const response = await axios.post('/api/auth/email/verify', {
|
||||
email: email.value,
|
||||
code: verificationCode.value
|
||||
}, {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
error.value = `Ошибка: ${response.data.error}`;
|
||||
return;
|
||||
}
|
||||
|
||||
success.value = 'Email успешно подтвержден';
|
||||
|
||||
// Сбрасываем форму
|
||||
setTimeout(() => {
|
||||
email.value = '';
|
||||
verificationCode.value = '';
|
||||
verificationSent.value = false;
|
||||
success.value = '';
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
console.error('Error verifying email:', err);
|
||||
error.value = 'Ошибка при проверке кода подтверждения';
|
||||
error.value = err.message || 'Ошибка отправки кода';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const verifyCode = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
await props.onEmailAuth(email.value, code.value);
|
||||
error.value = '';
|
||||
} catch (err) {
|
||||
error.value = err.message || 'Неверный код';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.email-connect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
.email-connection {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.email-form, .verification-form {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
.email-input,
|
||||
.code-input {
|
||||
padding: 8px;
|
||||
margin-right: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.connect-button, .verify-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 15px;
|
||||
background-color: #4caf50;
|
||||
.email-btn,
|
||||
.verify-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #48bb78;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.connect-button:hover, .verify-button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.connect-button:disabled, .verify-button:disabled {
|
||||
background-color: #cccccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.email-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading, .error, .success {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
background-color: #f8f9fa;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
color: #e53e3e;
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAuthStore } from '../../stores/auth';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const identities = ref({});
|
||||
const newIdentity = ref({ type: 'email', value: '' });
|
||||
const loading = ref(false);
|
||||
const error = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await fetch('/api/access/tokens', {
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await response.json();
|
||||
identities.value = data.identities || {};
|
||||
} catch (err) {
|
||||
error.value = 'Ошибка при загрузке идентификаторов';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function addIdentity() {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const success = await authStore.linkIdentity(
|
||||
newIdentity.value.type,
|
||||
newIdentity.value.value
|
||||
);
|
||||
|
||||
if (success) {
|
||||
identities.value = authStore.identities;
|
||||
newIdentity.value.value = '';
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = 'Ошибка при добавлении идентификатора';
|
||||
console.error(err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
84
frontend/src/components/identity/WalletConnection.vue
Normal file
84
frontend/src/components/identity/WalletConnection.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="wallet-connection">
|
||||
<button
|
||||
@click="connectWallet"
|
||||
:disabled="isLoading"
|
||||
class="wallet-btn"
|
||||
>
|
||||
{{ isAuthenticated ? 'Подключено' : 'Подключить кошелек' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { connectWithWallet } from '../../services/wallet';
|
||||
|
||||
// Определяем props
|
||||
const props = defineProps({
|
||||
isAuthenticated: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
// Определяем состояние
|
||||
const isLoading = ref(false);
|
||||
|
||||
const emit = defineEmits(['connect']);
|
||||
|
||||
// Метод подключения кошелька
|
||||
const connectWallet = async () => {
|
||||
if (isLoading.value) return;
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
// Получаем адрес кошелька
|
||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||
const address = accounts[0];
|
||||
|
||||
// Получаем nonce
|
||||
const nonceResponse = await api.get(`/api/auth/nonce?address=${address}`);
|
||||
const nonce = nonceResponse.data.nonce;
|
||||
|
||||
// Подписываем сообщение
|
||||
const message = `${window.location.host} wants you to sign in with your Ethereum account:\n${address.slice(0, 42)}...`;
|
||||
const signature = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [message, address]
|
||||
});
|
||||
|
||||
emit('connect', { address, signature, message });
|
||||
} catch (error) {
|
||||
console.error('Error connecting wallet:', error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wallet-connection {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.wallet-btn {
|
||||
padding: 10px 20px;
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.wallet-btn:hover:not(:disabled) {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.wallet-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
9
frontend/src/components/identity/index.js
Normal file
9
frontend/src/components/identity/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import TelegramConnect from './TelegramConnect.vue';
|
||||
import WalletConnection from './WalletConnection.vue';
|
||||
import EmailConnect from './EmailConnect.vue';
|
||||
|
||||
export {
|
||||
TelegramConnect,
|
||||
WalletConnection,
|
||||
EmailConnect
|
||||
};
|
||||
Reference in New Issue
Block a user