Описание изменений

This commit is contained in:
2025-03-19 17:18:03 +03:00
parent 04d027054d
commit 55e4d81c95
75 changed files with 2103 additions and 4861 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -0,0 +1,9 @@
import TelegramConnect from './TelegramConnect.vue';
import WalletConnection from './WalletConnection.vue';
import EmailConnect from './EmailConnect.vue';
export {
TelegramConnect,
WalletConnection,
EmailConnect
};