Files
DLE/frontend/src/views/ConnectWalletView.vue
2025-10-30 22:41:04 +03:00

382 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
Copyright (c) 2024-2025 Тарабанов Александр Викторович
All rights reserved.
This software is proprietary and confidential.
Unauthorized copying, modification, or distribution is prohibited.
For licensing inquiries: info@hb3-accelerator.com
Website: https://hb3-accelerator.com
GitHub: https://github.com/VC-HB3-Accelerator
-->
<template>
<div class="connect-wallet-container">
<div class="connect-wallet-card">
<!-- Loading состояние -->
<div v-if="loading" class="loading-state">
<div class="spinner"></div>
<p>Проверка токена...</p>
</div>
<!-- Токен валиден -->
<div v-else-if="tokenValid && !connected" class="connect-state">
<div class="icon">🔗</div>
<h1>Подключение кошелька</h1>
<div class="info-block">
<p class="provider-info">
Вы переходите из:
<strong>{{ providerName }}</strong>
</p>
<p class="description">
Подключите Web3 кошелек для сохранения истории сообщений и полного доступа к системе
</p>
</div>
<button
@click="connectWallet"
:disabled="connecting"
class="connect-button"
>
<span v-if="!connecting">Подключить MetaMask</span>
<span v-else>Подключение...</span>
</button>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="expires-info">
Ссылка истекает: {{ expiresAt }}
</div>
</div>
<!-- Токен истек или недействителен -->
<div v-else-if="!tokenValid" class="expired-state">
<div class="icon"></div>
<h1>Ссылка истекла</h1>
<p>Эта ссылка больше недействительна</p>
<p class="hint">
Запросите новую ссылку в боте, отправив команду
<code>/connect</code>
</p>
</div>
<!-- Успешно подключено -->
<div v-else-if="connected" class="success-state">
<div class="icon"></div>
<h1>Кошелек подключен!</h1>
<p>История сообщений перенесена</p>
<p class="stats" v-if="migrationStats">
Перенесено сообщений: {{ migrationStats.migrated }}
</p>
<button @click="goToChat" class="go-chat-button">
Перейти к чату
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ConnectWalletView',
data() {
return {
loading: true,
tokenValid: false,
connected: false,
connecting: false,
error: null,
provider: null,
expiresAt: null,
migrationStats: null
};
},
computed: {
providerName() {
const names = {
telegram: 'Telegram',
email: 'Email'
};
return names[this.provider] || this.provider;
}
},
async mounted() {
const token = this.$route.query.token;
if (!token) {
this.loading = false;
this.tokenValid = false;
return;
}
await this.checkToken(token);
},
methods: {
async checkToken(token) {
try {
const response = await fetch(`/api/identity/link-status/${token}`);
const data = await response.json();
this.tokenValid = data.valid;
this.provider = data.provider;
if (data.expiresAt) {
const expiresDate = new Date(data.expiresAt);
this.expiresAt = expiresDate.toLocaleString('ru-RU');
}
this.loading = false;
} catch (error) {
console.error('Ошибка проверки токена:', error);
this.error = 'Ошибка проверки токена';
this.loading = false;
this.tokenValid = false;
}
},
async connectWallet() {
try {
this.connecting = true;
this.error = null;
// Проверяем наличие MetaMask
if (!window.ethereum) {
this.error = 'MetaMask не установлен. Установите расширение MetaMask.';
this.connecting = false;
return;
}
// 1. Запрос аккаунтов
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
if (!accounts || accounts.length === 0) {
this.error = 'Не удалось получить адрес кошелька';
this.connecting = false;
return;
}
const address = accounts[0];
// 2. Получить подпись
const message = `Подключение кошелька к системе\nАдрес: ${address}\nВремя: ${new Date().toISOString()}`;
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, address]
});
// 3. Отправить на сервер
const token = this.$route.query.token;
const response = await fetch('/api/auth/wallet-with-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
address,
signature,
message,
token
})
});
const result = await response.json();
if (result.success) {
this.connected = true;
this.migrationStats = {
migrated: result.migratedMessages
};
// Через 2 секунды переходим в чат
setTimeout(() => {
this.goToChat();
}, 2000);
} else {
this.error = result.error || 'Ошибка подключения кошелька';
this.connecting = false;
}
} catch (error) {
console.error('Ошибка подключения кошелька:', error);
if (error.code === 4001) {
this.error = 'Вы отклонили запрос подписи';
} else {
this.error = 'Ошибка подключения кошелька. Попробуйте снова.';
}
this.connecting = false;
}
},
goToChat() {
this.$router.push('/chat');
}
}
};
</script>
<style scoped>
.connect-wallet-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.connect-wallet-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 500px;
width: 100%;
text-align: center;
}
.icon {
font-size: 64px;
margin-bottom: 20px;
}
h1 {
font-size: 28px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 16px;
}
.info-block {
margin: 24px 0;
}
.provider-info {
font-size: 16px;
color: #666;
margin-bottom: 12px;
}
.provider-info strong {
color: #667eea;
font-weight: 600;
}
.description {
font-size: 14px;
color: #888;
line-height: 1.6;
}
.connect-button,
.go-chat-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
padding: 14px 32px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
margin-top: 24px;
}
.connect-button:hover:not(:disabled),
.go-chat-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.connect-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-top: 16px;
font-size: 14px;
}
.expires-info {
margin-top: 20px;
font-size: 13px;
color: #999;
}
.loading-state {
padding: 40px 20px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
color: #666;
font-size: 14px;
}
.expired-state,
.success-state {
padding: 20px 0;
}
.hint {
background: #f5f5f5;
padding: 12px;
border-radius: 8px;
margin-top: 20px;
font-size: 14px;
color: #666;
}
.hint code {
background: #e0e0e0;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
color: #333;
}
.stats {
background: #f0f9ff;
color: #0369a1;
padding: 12px;
border-radius: 8px;
margin-top: 16px;
font-size: 14px;
font-weight: 500;
}
</style>