feat: новая функция
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
:message="message"
|
||||
:isPrivateChat="isPrivateChat"
|
||||
:currentUserId="currentUserId"
|
||||
@consent-granted="handleConsentGranted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,6 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -148,6 +150,7 @@ const emit = defineEmits([
|
||||
'send-message',
|
||||
'load-more', // Событие для загрузки старых сообщений
|
||||
'ai-reply',
|
||||
'remove-consent-messages', // Событие для удаления системных сообщений о согласиях
|
||||
]);
|
||||
|
||||
const messagesContainer = ref(null);
|
||||
@@ -155,6 +158,11 @@ const messageInputRef = ref(null);
|
||||
const chatInputRef = ref(null); // Ref для chat-input
|
||||
const chatInputHeight = ref(80); // Начальная высота (можно подобрать точнее)
|
||||
|
||||
function handleConsentGranted(messageId) {
|
||||
// После подписания удаляем системное сообщение о необходимости согласия
|
||||
emit('remove-consent-messages', [messageId]);
|
||||
}
|
||||
|
||||
// Локальное состояние для предпросмотра, синхронизированное с props.attachments
|
||||
const localAttachments = ref([...props.attachments]);
|
||||
watch(() => props.attachments, (newVal) => {
|
||||
|
||||
341
frontend/src/components/ConsentModal.vue
Normal file
341
frontend/src/components/ConsentModal.vue
Normal file
@@ -0,0 +1,341 @@
|
||||
<!--
|
||||
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 v-if="isOpen" class="consent-modal-overlay" @click.self="close">
|
||||
<div class="consent-modal">
|
||||
<div class="consent-modal-header">
|
||||
<h2>Подписание документов</h2>
|
||||
<button class="close-btn" @click="close">×</button>
|
||||
</div>
|
||||
|
||||
<div class="consent-modal-content">
|
||||
<p class="consent-description">
|
||||
Для полноценного использования сервиса необходимо ознакомиться и подписать следующие документы:
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<p>Загрузка документов...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="documents.length === 0" class="empty-state">
|
||||
<p>Документы не найдены</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="documents-list">
|
||||
<div v-for="doc in documents" :key="doc.id" class="document-item">
|
||||
<label class="document-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="doc.id"
|
||||
v-model="selectedDocuments"
|
||||
class="checkbox-input"
|
||||
/>
|
||||
<div class="document-info">
|
||||
<h3 class="document-title">{{ doc.title }}</h3>
|
||||
<p v-if="doc.summary" class="document-summary">{{ doc.summary }}</p>
|
||||
<a
|
||||
:href="`/content/published/${doc.id}`"
|
||||
target="_blank"
|
||||
class="document-link"
|
||||
@click.stop
|
||||
>
|
||||
Открыть документ →
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="consent-modal-footer">
|
||||
<button class="btn-secondary" @click="close">Отмена</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="submitConsent"
|
||||
:disabled="selectedDocuments.length === 0 || submitting"
|
||||
>
|
||||
{{ submitting ? 'Подписание...' : 'Подписать' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import api from '../api/axios';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
missingConsents: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'consent-granted']);
|
||||
|
||||
const documents = ref([]);
|
||||
const selectedDocuments = ref([]);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
// Загружаем документы для подписания
|
||||
async function loadDocuments() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await api.get('/consent/documents');
|
||||
documents.value = response.data || [];
|
||||
|
||||
// Автоматически выбираем все документы
|
||||
selectedDocuments.value = documents.value.map(doc => doc.id);
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки документов:', error);
|
||||
documents.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем согласие
|
||||
async function submitConsent() {
|
||||
if (selectedDocuments.value.length === 0) return;
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
// Получаем типы согласий для выбранных документов
|
||||
const consentTypes = documents.value
|
||||
.filter(doc => selectedDocuments.value.includes(doc.id))
|
||||
.map(doc => doc.consentType)
|
||||
.filter(type => type);
|
||||
|
||||
await api.post('/consent/grant', {
|
||||
documentIds: selectedDocuments.value,
|
||||
consentTypes: consentTypes,
|
||||
});
|
||||
|
||||
emit('consent-granted');
|
||||
close();
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания документов:', error);
|
||||
alert('Ошибка при подписании документов. Попробуйте еще раз.');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
emit('close');
|
||||
selectedDocuments.value = [];
|
||||
}
|
||||
|
||||
// Загружаем документы при открытии модалки
|
||||
watch(() => props.isOpen, (newValue) => {
|
||||
if (newValue) {
|
||||
loadDocuments();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isOpen) {
|
||||
loadDocuments();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.consent-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.consent-modal {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.consent-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.consent-modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-primary, #333);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.consent-modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.consent-description {
|
||||
margin: 0 0 20px 0;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.documents-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.document-item {
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.document-item:hover {
|
||||
border-color: var(--color-primary, #007bff);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.document-checkbox {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkbox-input {
|
||||
margin-top: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.document-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.document-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-primary, #333);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.document-summary {
|
||||
margin: 0 0 8px 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.document-link {
|
||||
color: var(--color-primary, #007bff);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.document-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.consent-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.btn-secondary,
|
||||
.btn-primary {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-primary, #007bff);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-primary-dark, #0056b3);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -46,8 +46,43 @@
|
||||
<a :href="replyLink" class="reply-link">Ответить</a>
|
||||
</div>
|
||||
|
||||
<!-- Блок с документами для подписания -->
|
||||
<div v-if="message.consentRequired && message.consentDocuments" class="consent-documents-block">
|
||||
<div v-for="doc in message.consentDocuments" :key="doc.id" class="consent-document-item">
|
||||
<label class="consent-document-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="doc.id"
|
||||
v-model="selectedConsentDocuments"
|
||||
class="consent-checkbox"
|
||||
/>
|
||||
<div class="consent-document-info">
|
||||
<h4 class="consent-document-title">{{ doc.title }}</h4>
|
||||
<p v-if="doc.summary" class="consent-document-summary">{{ doc.summary }}</p>
|
||||
<a
|
||||
:href="`/content/published/${doc.id}`"
|
||||
target="_blank"
|
||||
class="consent-document-link"
|
||||
@click.stop
|
||||
>
|
||||
Открыть документ →
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="consent-actions">
|
||||
<button
|
||||
@click="submitConsent"
|
||||
class="system-btn primary"
|
||||
:disabled="selectedConsentDocuments.length === 0 || isSubmittingConsent"
|
||||
>
|
||||
{{ isSubmittingConsent ? 'Подписание...' : 'Подписать' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки для системного сообщения -->
|
||||
<div v-if="message.sender_type === 'system' && (message.telegramBotUrl || message.supportEmail)" class="system-actions">
|
||||
<div v-if="message.sender_type === 'system' && (message.telegramBotUrl || message.supportEmail) && !message.consentRequired" class="system-actions">
|
||||
<button v-if="message.telegramBotUrl" @click="openTelegram(message.telegramBotUrl)" class="system-btn">Перейти в Telegram-бот</button>
|
||||
<button v-if="message.supportEmail" @click="copyEmail(message.supportEmail)" class="system-btn">Скопировать email</button>
|
||||
</div>
|
||||
@@ -117,6 +152,48 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['consent-granted']);
|
||||
|
||||
// Состояние для выбранных документов и отправки согласия
|
||||
const selectedConsentDocuments = ref([]);
|
||||
const isSubmittingConsent = ref(false);
|
||||
|
||||
// Инициализируем выбранные документы при монтировании, если есть документы
|
||||
watch(() => props.message.consentDocuments, (docs) => {
|
||||
if (docs && Array.isArray(docs) && docs.length > 0) {
|
||||
// Автоматически выбираем все документы
|
||||
selectedConsentDocuments.value = docs.map(doc => doc.id);
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// Функция подписания документов
|
||||
async function submitConsent() {
|
||||
if (selectedConsentDocuments.value.length === 0 || isSubmittingConsent.value) return;
|
||||
|
||||
isSubmittingConsent.value = true;
|
||||
try {
|
||||
const api = (await import('../api/axios')).default;
|
||||
const documents = props.message.consentDocuments || [];
|
||||
const consentTypes = documents
|
||||
.filter(doc => selectedConsentDocuments.value.includes(doc.id))
|
||||
.map(doc => doc.consentType)
|
||||
.filter(type => type);
|
||||
|
||||
await api.post('/consent/grant', {
|
||||
documentIds: selectedConsentDocuments.value,
|
||||
consentTypes: consentTypes,
|
||||
});
|
||||
|
||||
// Уведомляем родительский компонент об успешном подписании
|
||||
emit('consent-granted', props.message.id);
|
||||
} catch (error) {
|
||||
console.error('Ошибка подписания документов:', error);
|
||||
alert('Ошибка при подписании документов. Попробуйте еще раз.');
|
||||
} finally {
|
||||
isSubmittingConsent.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Простая функция для определения, является ли сообщение отправленным текущим пользователем
|
||||
// Используем данные из самого сообщения для определения направления
|
||||
const isCurrentUserMessage = computed(() => {
|
||||
@@ -494,6 +571,97 @@ function copyEmail(email) {
|
||||
.system-btn:hover {
|
||||
background: var(--color-primary-dark, #2563eb);
|
||||
}
|
||||
.system-btn.primary {
|
||||
background: var(--color-primary, #007bff);
|
||||
font-weight: 600;
|
||||
}
|
||||
.system-btn.primary:hover {
|
||||
background: var(--color-primary-dark, #0056b3);
|
||||
}
|
||||
.system-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Стили для блока с документами для подписания */
|
||||
.consent-documents-block {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.consent-document-item {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.consent-document-item:hover {
|
||||
border-color: var(--color-primary, #007bff);
|
||||
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.consent-document-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.consent-document-label {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.consent-checkbox {
|
||||
margin-top: 4px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.consent-document-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.consent-document-title {
|
||||
margin: 0 0 6px 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-primary, #333);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.consent-document-summary {
|
||||
margin: 0 0 8px 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.consent-document-link {
|
||||
color: var(--color-primary, #007bff);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.consent-document-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.consent-actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Стили для информации об отправителе в приватном чате */
|
||||
.message-sender-info {
|
||||
|
||||
@@ -316,6 +316,7 @@ export function useChat(auth) {
|
||||
}
|
||||
|
||||
// Добавляем ответ ИИ, если есть
|
||||
// Системное сообщение о согласиях уже включено в ответ ИИ
|
||||
if (response.data.aiResponse) {
|
||||
messages.value.push({
|
||||
id: `ai_${Date.now()}`,
|
||||
@@ -323,12 +324,17 @@ export function useChat(auth) {
|
||||
sender_type: 'assistant',
|
||||
role: 'assistant',
|
||||
timestamp: new Date().toISOString(),
|
||||
isLocal: false
|
||||
isLocal: false,
|
||||
// Добавляем информацию о согласиях, если есть
|
||||
consentRequired: response.data.consentRequired || false,
|
||||
missingConsents: response.data.missingConsents || [],
|
||||
consentDocuments: response.data.consentDocuments || [],
|
||||
autoConsentOnReply: response.data.autoConsentOnReply || false
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем системное сообщение для гостя (только на клиенте, не сохраняется в истории)
|
||||
if (isGuestMessage && response.data.systemMessage) {
|
||||
// Добавляем системное сообщение для гостя (только если нет согласия, чтобы не дублировать)
|
||||
if (isGuestMessage && response.data.systemMessage && !response.data.consentRequired) {
|
||||
messages.value.push({
|
||||
id: `system-${Date.now()}`,
|
||||
content: response.data.systemMessage,
|
||||
|
||||
@@ -48,12 +48,30 @@ export async function connectWithWallet() {
|
||||
throw new Error('Не удалось получить nonce с сервера');
|
||||
}
|
||||
|
||||
// Получаем список документов для подписания
|
||||
let resources = [`${window.location.origin}/api/auth/verify`];
|
||||
try {
|
||||
const docsResponse = await axios.get('/consent/documents');
|
||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||
docsResponse.data.forEach(doc => {
|
||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Если не удалось получить документы, продолжаем без них
|
||||
console.warn('Не удалось получить список документов для подписания:', error);
|
||||
}
|
||||
|
||||
// Создаем сообщение для подписи
|
||||
const domain = window.location.host;
|
||||
const origin = window.location.origin;
|
||||
const statement = 'Sign in with Ethereum to the app.';
|
||||
const statement = 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.';
|
||||
|
||||
const issuedAt = new Date().toISOString();
|
||||
|
||||
// Создаем копию resources и сортируем (не мутируем исходный массив)
|
||||
const sortedResources = [...resources].sort();
|
||||
|
||||
const siweMessage = new SiweMessage({
|
||||
domain,
|
||||
address,
|
||||
@@ -63,7 +81,7 @@ export async function connectWithWallet() {
|
||||
chainId: 1,
|
||||
nonce,
|
||||
issuedAt,
|
||||
resources: [`${origin}/api/auth/verify`],
|
||||
resources: sortedResources,
|
||||
});
|
||||
|
||||
const message = siweMessage.prepareMessage();
|
||||
|
||||
@@ -43,8 +43,9 @@ export const connectWallet = async () => {
|
||||
|
||||
// Берем первый аккаунт в списке
|
||||
const address = accounts[0];
|
||||
// Нормализуем адрес (приводим к нижнему регистру для последующих сравнений)
|
||||
const normalizedAddress = ethers.utils.getAddress(address);
|
||||
// Нормализуем адрес (используем getAddress для совместимости)
|
||||
// Проверяем версию ethers - если v6, используем ethers.getAddress, иначе ethers.utils.getAddress
|
||||
const normalizedAddress = ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address);
|
||||
// console.log('Normalized address:', normalizedAddress);
|
||||
|
||||
// Запрашиваем nonce с сервера
|
||||
@@ -60,34 +61,70 @@ export const connectWallet = async () => {
|
||||
};
|
||||
}
|
||||
|
||||
// Создаем провайдер Ethers
|
||||
const provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||
const signer = provider.getSigner();
|
||||
// Для SIWE используем personal_sign напрямую через window.ethereum
|
||||
// Не используем ethers signer, так как он добавляет префикс, который нарушает SIWE формат
|
||||
|
||||
// Получаем список документов для подписания
|
||||
let resources = [`${window.location.origin}/api/auth/verify`];
|
||||
try {
|
||||
const docsResponse = await axios.get('/consent/documents');
|
||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||
docsResponse.data.forEach(doc => {
|
||||
resources.push(`${window.location.origin}/content/published/${doc.id}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Если не удалось получить документы, продолжаем без них
|
||||
console.warn('Не удалось получить список документов для подписания:', error);
|
||||
}
|
||||
|
||||
// Создаем сообщение для подписи
|
||||
const domain = window.location.host;
|
||||
// Важно: domain должен быть hostname без протокола и порта (если порт стандартный)
|
||||
const domain = window.location.hostname === 'localhost' ?
|
||||
`localhost:${window.location.port}` :
|
||||
window.location.hostname;
|
||||
const origin = window.location.origin;
|
||||
|
||||
// Создаем issuedAt один раз, чтобы использовать одинаковый в сообщении и запросе
|
||||
const issuedAt = new Date().toISOString();
|
||||
|
||||
// Создаем SIWE сообщение
|
||||
// Создаем копию resources и сортируем (не мутируем исходный массив)
|
||||
const sortedResources = [...resources].sort();
|
||||
|
||||
// Создаем SIWE сообщение с документами в resources
|
||||
const message = new SiweMessage({
|
||||
domain,
|
||||
address: normalizedAddress,
|
||||
statement: 'Sign in with Ethereum to the app.',
|
||||
statement: 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.',
|
||||
uri: origin,
|
||||
version: '1',
|
||||
chainId: 1, // Ethereum mainnet
|
||||
nonce: nonce,
|
||||
issuedAt: new Date().toISOString(),
|
||||
resources: [`${origin}/api/auth/verify`],
|
||||
issuedAt: issuedAt,
|
||||
resources: sortedResources,
|
||||
});
|
||||
|
||||
// Получаем строку сообщения для подписи
|
||||
const messageToSign = message.prepareMessage();
|
||||
// console.log('SIWE message:', messageToSign);
|
||||
|
||||
// Логируем для отладки
|
||||
console.log('🔐 [Frontend] Domain:', domain);
|
||||
console.log('🔐 [Frontend] Origin:', origin);
|
||||
console.log('🔐 [Frontend] Address:', normalizedAddress);
|
||||
console.log('🔐 [Frontend] Nonce:', nonce);
|
||||
console.log('🔐 [Frontend] IssuedAt:', issuedAt);
|
||||
console.log('🔐 [Frontend] Resources:', JSON.stringify(sortedResources));
|
||||
console.log('🔐 [Frontend] SIWE message to sign:', messageToSign);
|
||||
console.log('🔐 [Frontend] Message length:', messageToSign.length);
|
||||
|
||||
// Запрашиваем подпись
|
||||
// console.log('Requesting signature...');
|
||||
const signature = await signer.signMessage(messageToSign);
|
||||
// Запрашиваем подпись через personal_sign (правильный способ для SIWE)
|
||||
// personal_sign подписывает сообщение С префиксом "\x19Ethereum Signed Message:\n"
|
||||
// ethers.verifyMessage() также добавляет этот префикс, поэтому они совместимы
|
||||
// Параметры: [message, address] - MetaMask принимает строку напрямую
|
||||
const signature = await window.ethereum.request({
|
||||
method: 'personal_sign',
|
||||
params: [messageToSign, normalizedAddress.toLowerCase()],
|
||||
});
|
||||
|
||||
if (!signature) {
|
||||
return {
|
||||
@@ -104,7 +141,7 @@ export const connectWallet = async () => {
|
||||
address: normalizedAddress,
|
||||
signature,
|
||||
nonce,
|
||||
issuedAt: new Date().toISOString(),
|
||||
issuedAt: issuedAt, // Используем тот же issuedAt, что и в сообщении
|
||||
};
|
||||
// console.log('Request data:', requestData);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
v-model:attachments="attachments"
|
||||
@send-message="handleSendMessage"
|
||||
@load-more="loadMessages"
|
||||
@remove-consent-messages="handleRemoveConsentMessages"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -50,6 +51,7 @@
|
||||
v-model:attachments="attachments"
|
||||
@send-message="handleSendMessage"
|
||||
@load-more="loadMessages"
|
||||
@remove-consent-messages="handleRemoveConsentMessages"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
@@ -155,6 +157,13 @@
|
||||
loadMessages({ initial: true });
|
||||
}
|
||||
};
|
||||
|
||||
// Функция удаления системных сообщений о согласиях после подписания
|
||||
const handleRemoveConsentMessages = (messageIds) => {
|
||||
if (messageIds && Array.isArray(messageIds)) {
|
||||
messages.value = messages.value.filter(msg => !messageIds.includes(msg.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user