ваше сообщение коммита

This commit is contained in:
2025-04-10 18:27:06 +03:00
parent 70c0a50c44
commit 4d8ec5c914
11 changed files with 979 additions and 482 deletions

View File

@@ -1,6 +1,6 @@
<template>
<div class="email-connection">
<div v-if="!showVerification">
<div v-if="!showVerification" class="email-form">
<input
v-model="email"
type="email"
@@ -12,10 +12,11 @@
:disabled="isLoading || !isValidEmail"
class="email-btn"
>
Получить код
{{ isLoading ? 'Отправка...' : 'Получить код' }}
</button>
</div>
<div v-else>
<div v-else class="verification-form">
<p class="verification-info">Код отправлен на {{ email }}</p>
<input
v-model="code"
type="text"
@@ -24,10 +25,16 @@
/>
<button
@click="verifyCode"
:disabled="isLoading"
:disabled="isLoading || !code"
class="verify-btn"
>
Подтвердить
{{ isLoading ? 'Проверка...' : 'Подтвердить' }}
</button>
<button
@click="resetForm"
class="reset-btn"
>
Изменить email
</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
@@ -36,21 +43,17 @@
<script setup>
import { ref, computed } from 'vue';
import axios from 'axios';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
const props = defineProps({
onEmailAuth: {
type: Function,
required: true
}
});
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const email = ref('');
const code = ref('');
const error = ref('');
const isLoading = ref(false);
const showVerification = ref(false);
const isConnecting = ref(false);
const isValidEmail = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value);
@@ -59,11 +62,19 @@ const isValidEmail = computed(() => {
const requestCode = async () => {
try {
isLoading.value = true;
await props.onEmailAuth(email.value);
showVerification.value = true;
error.value = '';
const response = await axios.post('/api/auth/email/request-verification', {
email: email.value
});
if (response.data.success) {
showVerification.value = true;
} else {
error.value = response.data.error || 'Ошибка отправки кода';
}
} catch (err) {
error.value = err.message || 'Ошибка отправки кода';
error.value = err.response?.data?.error || 'Ошибка отправки кода';
} finally {
isLoading.value = false;
}
@@ -72,37 +83,80 @@ const requestCode = async () => {
const verifyCode = async () => {
try {
isLoading.value = true;
await props.onEmailAuth(email.value, code.value);
error.value = '';
const response = await axios.post('/api/auth/email/verify', {
email: email.value,
code: code.value
});
if (response.data.success) {
// Связываем email с текущим пользователем
await linkIdentity('email', email.value);
emit('close');
} else {
error.value = response.data.error || 'Неверный код';
}
} catch (err) {
error.value = err.message || 'Неверный код';
error.value = err.response?.data?.error || 'Ошибка проверки кода';
} finally {
isLoading.value = false;
}
};
const resetForm = () => {
email.value = '';
code.value = '';
error.value = '';
showVerification.value = false;
};
</script>
<style scoped>
.email-connection {
margin: 10px 0;
padding: 20px;
max-width: 400px;
}
.email-form,
.verification-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.email-input,
.code-input {
padding: 8px;
margin-right: 10px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.email-btn,
.verify-btn,
.reset-btn {
padding: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.email-btn,
.verify-btn {
padding: 10px 20px;
background-color: #48bb78;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.reset-btn {
background-color: #e2e8f0;
color: #4a5568;
}
.verification-info {
color: #4a5568;
font-size: 14px;
}

View File

@@ -1,68 +1,135 @@
<template>
<div class="telegram-connect">
<p>Подключите свой аккаунт Telegram для быстрой авторизации.</p>
<button @click="connectTelegram" class="connect-button">
<span class="telegram-icon">📱</span> Подключить Telegram
</button>
<div v-if="loading" class="loading">Загрузка...</div>
<div v-if="!showQR" class="intro">
<p>Подключите свой аккаунт Telegram для быстрой авторизации</p>
<button @click="startConnection" class="connect-button" :disabled="loading">
<span class="telegram-icon">📱</span>
{{ loading ? 'Загрузка...' : 'Подключить Telegram' }}
</button>
</div>
<div v-else class="qr-section">
<p>Отсканируйте QR-код в приложении Telegram</p>
<div class="qr-container" v-html="qrCode"></div>
<p class="or-divider">или</p>
<a :href="botLink" target="_blank" class="bot-link">
Открыть бота в Telegram
</a>
<button @click="resetConnection" class="reset-button">
Отмена
</button>
</div>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="success" class="success">{{ success }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '@/api/axios';
import { useAuth } from '@/composables/useAuth';
import QRCode from 'qrcode';
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
const loading = ref(false);
const error = ref('');
const success = ref('');
const isConnecting = ref(false);
const showQR = ref(false);
const qrCode = ref('');
const botLink = ref('');
const pollInterval = ref(null);
const connectionToken = ref('');
async function connectTelegram() {
const startConnection = async () => {
try {
loading.value = true;
error.value = '';
success.value = '';
// Запрос на получение ссылки для авторизации через Telegram
const response = await axios.get('/api/auth/telegram', {
withCredentials: true
});
const response = await axios.post('/api/auth/telegram/start-connection');
if (response.data.error) {
error.value = `Ошибка при подключении Telegram: ${response.data.error}`;
return;
}
if (response.data.authUrl) {
success.value = 'Перейдите по ссылке для авторизации через Telegram';
window.open(response.data.authUrl, '_blank');
if (response.data.success) {
connectionToken.value = response.data.token;
botLink.value = `https://t.me/${response.data.botUsername}?start=${connectionToken.value}`;
// Генерируем QR-код
const qr = await QRCode.toDataURL(botLink.value);
qrCode.value = `<img src="${qr}" alt="Telegram QR Code" />`;
showQR.value = true;
startPolling();
} else {
error.value = 'Не удалось получить ссылку для авторизации';
error.value = response.data.error || 'Не удалось начать процесс подключения';
}
} catch (err) {
console.error('Error connecting Telegram:', err);
error.value = 'Ошибка при подключении Telegram';
error.value = err.response?.data?.error || 'Ошибка при подключении Telegram';
} finally {
loading.value = false;
}
}
};
const checkConnection = async () => {
try {
const response = await axios.post('/api/auth/telegram/check-connection', {
token: connectionToken.value
});
if (response.data.success && response.data.telegramId) {
// Связываем Telegram с текущим пользователем
await linkIdentity('telegram', response.data.telegramId);
stopPolling();
emit('close');
}
} catch (error) {
console.error('Error checking connection:', error);
}
};
const startPolling = () => {
pollInterval.value = setInterval(checkConnection, 2000);
};
const stopPolling = () => {
if (pollInterval.value) {
clearInterval(pollInterval.value);
pollInterval.value = null;
}
};
const resetConnection = () => {
stopPolling();
showQR.value = false;
error.value = '';
qrCode.value = '';
botLink.value = '';
connectionToken.value = '';
};
onUnmounted(() => {
stopPolling();
});
</script>
<style scoped>
.telegram-connect {
padding: 20px;
max-width: 400px;
}
.intro,
.qr-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.connect-button {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 15px;
padding: 10px 20px;
background-color: #0088cc;
color: white;
border: none;
@@ -72,7 +139,7 @@ async function connectTelegram() {
transition: background-color 0.2s;
}
.connect-button:hover {
.connect-button:hover:not(:disabled) {
background-color: #0077b5;
}
@@ -81,23 +148,60 @@ async function connectTelegram() {
font-size: 18px;
}
.loading, .error, .success {
.qr-container {
background: white;
padding: 10px;
border-radius: 4px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.loading {
background-color: #f8f9fa;
.qr-container img {
max-width: 200px;
height: auto;
}
.or-divider {
color: #666;
margin: 10px 0;
}
.bot-link {
color: #0088cc;
text-decoration: none;
padding: 8px 16px;
border: 1px solid #0088cc;
border-radius: 4px;
transition: all 0.2s;
}
.bot-link:hover {
background-color: #0088cc;
color: white;
}
.reset-button {
padding: 8px 16px;
background-color: #e2e8f0;
color: #4a5568;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.reset-button:hover {
background-color: #cbd5e0;
}
.error {
background-color: #f8d7da;
color: #721c24;
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
.success {
background-color: #d4edda;
color: #155724;
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -1,53 +1,65 @@
<template>
<div class="wallet-connection">
<button
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
{{ isConnected ? 'Подключено' : 'Подключить кошелек' }}
</button>
<div v-if="!isConnected" class="connect-section">
<p>Подключите свой кошелек для доступа к расширенным функциям</p>
<button
@click="connectWallet"
:disabled="isLoading"
class="wallet-btn"
>
<span class="wallet-icon">💳</span>
{{ isLoading ? 'Подключение...' : 'Подключить кошелек' }}
</button>
</div>
<div v-else class="status-section">
<p>Кошелек подключен</p>
<p class="address">{{ formatAddress(address) }}</p>
</div>
<div v-if="error" class="error">{{ error }}</div>
</div>
</template>
<script setup>
import { ref, inject, computed } from 'vue';
import { connectWithWallet } from '../../services/wallet';
import { ethers } from 'ethers';
import { SiweMessage } from 'siwe';
import axios from 'axios';
import { ref, computed } from 'vue';
import { useAuth } from '@/composables/useAuth';
import { connectWithWallet } from '@/services/wallet';
// Определяем props
const props = defineProps({
isAuthenticated: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['close']);
const { linkIdentity } = useAuth();
// Определяем состояние
const isLoading = ref(false);
const auth = inject('auth');
const isConnecting = ref(false);
const error = ref('');
const address = ref('');
// Вычисляемое свойство для статуса подключения
const isConnected = computed(() => auth.isAuthenticated.value);
const isConnected = computed(() => !!address.value);
const emit = defineEmits(['connect']);
const formatAddress = (addr) => {
if (!addr) return '';
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
// Метод подключения кошелька
const connectWallet = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
error.value = '';
// Подключаем кошелек
const result = await connectWithWallet();
await auth.checkAuth();
console.log('Wallet connected, auth state:', auth.isAuthenticated.value);
emit('connect', result);
} catch (error) {
console.error('Error connecting wallet:', error);
if (result.success) {
address.value = result.address;
// Связываем кошелек с текущим пользователем
await linkIdentity('wallet', result.address);
emit('close');
} else {
error.value = result.error || 'Не удалось подключить кошелек';
}
} catch (err) {
console.error('Error connecting wallet:', err);
error.value = err.message || 'Произошла ошибка при подключении кошелька';
} finally {
isLoading.value = false;
}
@@ -56,25 +68,57 @@ const connectWallet = async () => {
<style scoped>
.wallet-connection {
margin: 10px 0;
padding: 20px;
max-width: 400px;
}
.connect-section,
.status-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
text-align: center;
}
.wallet-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
background-color: #4a5568;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
font-size: 16px;
transition: all 0.2s;
}
.wallet-btn:hover:not(:disabled) {
background-color: #2d3748;
}
.wallet-btn:disabled {
.wallet-icon {
margin-right: 10px;
font-size: 18px;
}
.address {
font-family: monospace;
background-color: #f7fafc;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.error {
color: #e53e3e;
margin-top: 10px;
text-align: center;
}
button:disabled {
opacity: 0.7;
cursor: not-allowed;
}

View File

@@ -1,4 +1,4 @@
import { ref, onMounted } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import axios from '../api/axios';
export function useAuth() {
@@ -20,7 +20,19 @@ export function useAuth() {
try {
const response = await axios.get('/api/auth/identities');
if (response.data.success) {
identities.value = response.data.identities;
// Фильтруем идентификаторы: убираем гостевые и оставляем только уникальные
const filteredIdentities = response.data.identities
.filter(identity => identity.provider !== 'guest')
.reduce((acc, identity) => {
// Для каждого типа провайдера оставляем только один идентификатор
const existingIdentity = acc.find(i => i.provider === identity.provider);
if (!existingIdentity) {
acc.push(identity);
}
return acc;
}, []);
identities.value = filteredIdentities;
console.log('User identities updated:', identities.value);
}
} catch (error) {
@@ -28,6 +40,21 @@ export function useAuth() {
}
};
// Периодическое обновление идентификаторов
let identitiesInterval;
const startIdentitiesPolling = () => {
if (identitiesInterval) return;
identitiesInterval = setInterval(updateIdentities, 30000); // Обновляем каждые 30 секунд
};
const stopIdentitiesPolling = () => {
if (identitiesInterval) {
clearInterval(identitiesInterval);
identitiesInterval = null;
}
};
const checkTokenBalances = async (address) => {
try {
const response = await axios.get(`/api/auth/check-tokens/${address}`);
@@ -81,6 +108,15 @@ export function useAuth() {
await checkTokenBalances(newAddress);
}
// Обновляем идентификаторы при любом изменении аутентификации
if (authenticated) {
await updateIdentities();
startIdentitiesPolling();
} else {
stopIdentitiesPolling();
identities.value = [];
}
console.log('Auth updated:', {
authenticated: isAuthenticated.value,
userId: userId.value,
@@ -91,11 +127,10 @@ export function useAuth() {
});
// Если пользователь только что аутентифицировался или сменил аккаунт,
// пробуем связать сообщения и обновить идентификаторы
// пробуем связать сообщения
if (authenticated && (!wasAuthenticated || (previousUserId && previousUserId !== newUserId))) {
console.log('Auth change detected, linking messages and updating identities');
console.log('Auth change detected, linking messages');
linkMessages();
updateIdentities();
}
};
@@ -343,6 +378,11 @@ export function useAuth() {
onMounted(async () => {
await checkAuth();
});
// Очищаем интервал при размонтировании компонента
onUnmounted(() => {
stopIdentitiesPolling();
});
return {
isAuthenticated,

View File

@@ -97,22 +97,22 @@
</div>
<!-- Добавляем дополнительные кнопки авторизации -->
<div v-if="!isAuthenticated && messages.length > 0" class="auth-buttons">
<div v-if="!isAuthenticated" class="auth-buttons">
<h3>Авторизация через:</h3>
<div v-if="!showTelegramVerification" class="auth-btn-container">
<button @click="handleTelegramAuth" class="auth-btn telegram-btn">
Подключить Telegram
</button>
</div>
<div v-if="showTelegramVerification" class="verification-block">
<div class="verification-code">
<div v-if="showTelegramVerification" class="verification-block">
<div class="verification-code">
<span>Код верификации:</span>
<code @click="copyCode(telegramVerificationCode)">{{ telegramVerificationCode }}</code>
<code @click="copyCode(telegramVerificationCode)">{{ telegramVerificationCode }}</code>
<span v-if="codeCopied" class="copied-message">Скопировано!</span>
</div>
</div>
<a :href="telegramBotLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
</div>
</div>
<!-- Сообщение об ошибке в Telegram -->
<div v-if="telegramError" class="error-message">
@@ -124,9 +124,9 @@
<button @click="handleEmailAuth" class="auth-btn email-btn">
Подключить Email
</button>
</div>
</div>
<!-- Форма для Email верификации (встроена в auth-buttons) -->
<!-- Форма для Email верификации -->
<div v-if="showEmailForm" class="email-form">
<p>Введите ваш email для получения кода подтверждения:</p>
<div class="email-form-container">
@@ -139,15 +139,15 @@
/>
<button @click="sendEmailVerification" class="send-email-btn" :disabled="isEmailSending">
{{ isEmailSending ? 'Отправка...' : 'Отправить код' }}
</button>
</div>
</button>
</div>
<div class="form-actions">
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
<p v-if="emailFormatError" class="email-format-error">Пожалуйста, введите корректный email</p>
</div>
</div>
</div>
</div>
<!-- Форма для ввода кода верификации Email (встроена в auth-buttons) -->
<!-- Форма для ввода кода верификации Email -->
<div v-if="showEmailVerificationInput" class="email-verification-form">
<p>На ваш email <strong>{{ emailVerificationEmail }}</strong> отправлен код подтверждения.</p>
<div class="email-form-container">
@@ -164,42 +164,48 @@
</div>
<button @click="cancelEmailAuth" class="cancel-btn">Отмена</button>
</div>
</div>
</div>
<!-- Сообщение об ошибке в Email -->
<div v-if="emailError" class="error-message">
{{ emailError }}
<button class="close-error" @click="clearEmailError">×</button>
</div>
<div v-if="emailError" class="error-message">
{{ emailError }}
<button class="close-error" @click="clearEmailError">×</button>
</div>
<!-- Блок информации о пользователе -->
<div v-if="isAuthenticated" class="user-info">
<h3>Идентификаторы:</h3>
<div class="user-info-item">
<span class="user-info-label">Кошелек:</span>
<span v-if="auth.address?.value" class="user-info-value">{{ truncateAddress(auth.address.value) }}</span>
<span v-if="hasIdentityType('wallet')" class="user-info-value">
{{ truncateAddress(getIdentityValue('wallet')) }}
</span>
<button v-else @click="handleWalletAuth" class="connect-btn">
Подключить кошелек
</button>
</div>
<div class="user-info-item">
<span class="user-info-label">Telegram:</span>
<span v-if="auth.telegramId?.value" class="user-info-value">{{ auth.telegramId.value }}</span>
<span v-if="hasIdentityType('telegram')" class="user-info-value">
{{ getIdentityValue('telegram') }}
</span>
<button v-else @click="handleTelegramAuth" class="connect-btn">
Подключить Telegram
</button>
</div>
<div class="user-info-item">
<span class="user-info-label">Email:</span>
<span v-if="auth.email?.value" class="user-info-value">{{ auth.email.value }}</span>
<span v-if="hasIdentityType('email')" class="user-info-value">
{{ getIdentityValue('email') }}
</span>
<button v-else @click="handleEmailAuth" class="connect-btn">
Подключить Email
</button>
</div>
</div>
<!-- Блок форм подключения -->
<div v-if="showEmailForm || showTelegramVerification || showEmailVerificationInput" class="connect-forms">
<!-- Блок форм подключения для аутентифицированных пользователей -->
<div v-if="isAuthenticated && (showEmailForm || showTelegramVerification || showEmailVerificationInput)" class="connect-forms">
<!-- Форма для Email верификации -->
<div v-if="showEmailForm" class="email-form">
<p>Введите ваш email для получения кода подтверждения:</p>
@@ -249,16 +255,6 @@
<a :href="telegramBotLink" target="_blank" class="bot-link">Открыть бота Telegram</a>
<button @click="cancelTelegramAuth" class="cancel-btn">Отмена</button>
</div>
<!-- Сообщения об ошибках -->
<div v-if="telegramError" class="error-message">
{{ telegramError }}
<button class="close-error" @click="telegramError = ''">×</button>
</div>
<div v-if="emailError" class="error-message">
{{ emailError }}
<button class="close-error" @click="clearEmailError">×</button>
</div>
</div>
<!-- Блок баланса токенов -->
@@ -542,6 +538,9 @@ const sendEmailVerification = async () => {
console.log('Showing verification code input form for email:', emailVerificationEmail.value);
} else {
emailError.value = response.data.error || 'Ошибка инициализации аутентификации по email';
// Возвращаем форму ввода email в исходное состояние
showEmailForm.value = true;
showEmailVerificationInput.value = false;
}
} catch (error) {
console.error('Error in email init request:', error);
@@ -550,6 +549,9 @@ const sendEmailVerification = async () => {
} else {
emailError.value = 'Ошибка при запросе кода подтверждения';
}
// Возвращаем форму ввода email в исходное состояние
showEmailForm.value = true;
showEmailVerificationInput.value = false;
} finally {
isEmailSending.value = false;
}
@@ -1401,6 +1403,18 @@ const cancelEmailAuth = () => {
emailFormatError.value = false;
};
// Методы для работы с идентификаторами
const hasIdentityType = (type) => {
if (!auth.identities?.value) return false;
return auth.identities.value.some(identity => identity.provider === type);
};
const getIdentityValue = (type) => {
if (!auth.identities?.value) return null;
const identity = auth.identities.value.find(identity => identity.provider === type);
return identity ? identity.provider_id : null;
};
// Функции жизненного цикла
onMounted(async () => {
console.log('HomeView.vue: компонент загружен');