feat: новая функция

This commit is contained in:
2025-10-09 16:48:20 +03:00
parent dd2c9988a5
commit 13fb51e447
60 changed files with 7694 additions and 1157 deletions

View File

@@ -0,0 +1,369 @@
<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>