ваше сообщение коммита
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -216,3 +216,8 @@ dle-template.tar.gz.join.sh
|
|||||||
|
|
||||||
# Guide files
|
# Guide files
|
||||||
pages-guide.md
|
pages-guide.md
|
||||||
|
|
||||||
|
# Problem documentation (internal)
|
||||||
|
SIWE_PROBLEM.md
|
||||||
|
REBUILD_COMMANDS.md
|
||||||
|
SIWE_ISSUE_DESCRIPTION.md
|
||||||
@@ -46,6 +46,16 @@ router.get('/nonce', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'Address is required' });
|
return res.status(400).json({ error: 'Address is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Нормализуем адрес: сначала через ethers.getAddress (EIP-55 checksum), потом toLowerCase для хранения
|
||||||
|
// Это гарантирует, что адрес валиден и всегда сохраняется в нижнем регистре
|
||||||
|
let normalizedAddress;
|
||||||
|
try {
|
||||||
|
normalizedAddress = ethers.getAddress(address).toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[nonce] Invalid address format: ${address}`, error);
|
||||||
|
return res.status(400).json({ error: 'Invalid address format' });
|
||||||
|
}
|
||||||
|
|
||||||
// Очищаем истекшие nonce перед генерацией нового
|
// Очищаем истекшие nonce перед генерацией нового
|
||||||
try {
|
try {
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
@@ -70,33 +80,34 @@ router.get('/nonce', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Проверяем, существует ли уже nonce для этого адреса
|
// Проверяем, существует ли уже nonce для этого адреса
|
||||||
|
// Используем normalizedAddress (уже в нижнем регистре)
|
||||||
const existingNonces = await db.getQuery()(
|
const existingNonces = await db.getQuery()(
|
||||||
'SELECT id FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
'SELECT id FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
||||||
[address.toLowerCase(), encryptionKey]
|
[normalizedAddress, encryptionKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingNonces.rows.length > 0) {
|
if (existingNonces.rows.length > 0) {
|
||||||
// Обновляем существующий nonce
|
// Обновляем существующий nonce
|
||||||
logger.info(`[nonce] Updating existing nonce for address: ${address.toLowerCase()}`);
|
logger.info(`[nonce] Updating existing nonce for address: ${normalizedAddress}`);
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2), expires_at = $3 WHERE id = $4',
|
'UPDATE nonces SET nonce_encrypted = encrypt_text($1, $2), expires_at = $3 WHERE id = $4',
|
||||||
[nonce, encryptionKey, new Date(Date.now() + 15 * 60 * 1000), existingNonces.rows[0].id]
|
[nonce, encryptionKey, new Date(Date.now() + 15 * 60 * 1000), existingNonces.rows[0].id]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Создаем новый nonce
|
// Создаем новый nonce
|
||||||
logger.info(`[nonce] Creating new nonce for address: ${address.toLowerCase()}`);
|
logger.info(`[nonce] Creating new nonce for address: ${normalizedAddress}`);
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
'INSERT INTO nonces (identity_value_encrypted, nonce_encrypted, expires_at) VALUES (encrypt_text($1, $2), encrypt_text($3, $2), $4)',
|
'INSERT INTO nonces (identity_value_encrypted, nonce_encrypted, expires_at) VALUES (encrypt_text($1, $2), encrypt_text($3, $2), $4)',
|
||||||
[address.toLowerCase(), encryptionKey, nonce, new Date(Date.now() + 15 * 60 * 1000)]
|
[normalizedAddress, encryptionKey, nonce, new Date(Date.now() + 15 * 60 * 1000)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error('Database error:', dbError);
|
console.error('Database error:', dbError);
|
||||||
// Fallback: просто возвращаем nonce без сохранения в БД
|
// Fallback: просто возвращаем nonce без сохранения в БД
|
||||||
logger.warn(`Nonce ${nonce} generated for address ${address} but not saved to DB due to error`);
|
logger.warn(`Nonce ${nonce} generated for address ${normalizedAddress} but not saved to DB due to error`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Nonce ${nonce} сохранен для адреса ${address}`);
|
logger.info(`Nonce ${nonce} сохранен для адреса ${normalizedAddress}`);
|
||||||
|
|
||||||
res.json({ nonce });
|
res.json({ nonce });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -136,6 +147,8 @@ router.post('/verify', async (req, res) => {
|
|||||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||||
|
|
||||||
// Проверяем nonce в базе данных с проверкой времени истечения
|
// Проверяем nonce в базе данных с проверкой времени истечения
|
||||||
|
// ВАЖНО: nonce привязан к адресу, поэтому проверяем для того адреса, который пришел в запросе
|
||||||
|
logger.info(`[verify] Checking nonce for address: ${normalizedAddressLower} (normalized from: ${address})`);
|
||||||
const nonceResult = await db.getQuery()(
|
const nonceResult = await db.getQuery()(
|
||||||
'SELECT nonce_encrypted, expires_at FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
'SELECT nonce_encrypted, expires_at FROM nonces WHERE identity_value_encrypted = encrypt_text($1, $2)',
|
||||||
[normalizedAddressLower, encryptionKey]
|
[normalizedAddressLower, encryptionKey]
|
||||||
@@ -143,7 +156,8 @@ router.post('/verify', async (req, res) => {
|
|||||||
|
|
||||||
if (nonceResult.rows.length === 0) {
|
if (nonceResult.rows.length === 0) {
|
||||||
logger.error(`[verify] Nonce not found for address: ${normalizedAddressLower}`);
|
logger.error(`[verify] Nonce not found for address: ${normalizedAddressLower}`);
|
||||||
return res.status(401).json({ success: false, error: 'Nonce not found' });
|
logger.error(`[verify] This may happen if user switched wallet between nonce request and signature`);
|
||||||
|
return res.status(401).json({ success: false, error: 'Nonce not found. Please request a new nonce.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем, не истек ли срок действия nonce
|
// Проверяем, не истек ли срок действия nonce
|
||||||
@@ -173,46 +187,32 @@ router.post('/verify', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ВАЖНО: Для SIWE сообщения ВСЕГДА используем хост из запроса, чтобы он совпадал с фронтендом
|
// ВАЖНО: Для SIWE сообщения ВСЕГДА используем хост из запроса, чтобы он совпадал с фронтендом
|
||||||
// Фронтенд использует window.location.host и window.location.origin, поэтому бэкенд должен использовать то же самое
|
// Фронтенд использует window.location.host, поэтому бэкенд должен использовать req.get('host')
|
||||||
// Это означает, что даже если в БД есть домен (например, 185.221.214.140), для SIWE будет использоваться
|
// Логика формирования domain должна точно соответствовать фронтенду
|
||||||
// хост из текущего запроса (например, localhost:9000), если запрос приходит с localhost
|
|
||||||
const protocol = req.protocol || 'http';
|
const protocol = req.protocol || 'http';
|
||||||
let host = req.get('host') || 'localhost:9000';
|
let host = req.get('host') || 'localhost:9000';
|
||||||
|
|
||||||
logger.info(`[verify] Request protocol: ${protocol}, host header: ${req.get('host')}, original host: ${host}`);
|
logger.info(`[verify] Request protocol: ${protocol}, host header: ${req.get('host')}`);
|
||||||
|
|
||||||
// Убеждаемся, что порт присутствует для localhost
|
// Формируем domain для SIWE (должен совпадать с фронтендом)
|
||||||
if (host === 'localhost' || host.startsWith('localhost:')) {
|
// Используем ту же логику, что и на фронтенде: window.location.host + добавление порта для localhost/IP
|
||||||
if (!host.includes(':')) {
|
let domain = host;
|
||||||
// Если порта нет, добавляем стандартный порт для протокола
|
|
||||||
const defaultPort = protocol === 'https' ? '443' : '9000';
|
|
||||||
host = `${host}:${defaultPort}`;
|
|
||||||
logger.info(`[verify] Added default port to localhost: ${host}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Формируем domain и origin для SIWE сообщения из текущего запроса
|
|
||||||
// domain - это host (например, "localhost:9000" или "example.com:443")
|
|
||||||
// ВАЖНО: domain и origin для SIWE НИКОГДА не берутся из БД, только из запроса!
|
|
||||||
const baseUrlForResources = `${protocol}://${host}`;
|
|
||||||
|
|
||||||
// Извлекаем домен и origin из baseUrlForResources для SIWE сообщения
|
|
||||||
const baseUrlObj = new URL(baseUrlForResources);
|
|
||||||
// Используем host (включает порт, если он нестандартный) или hostname + port
|
|
||||||
let domain = baseUrlObj.host; // Домен для SIWE (например, "localhost:9000" или "example.com")
|
|
||||||
// Если порт стандартный (80 для http, 443 для https), он может не быть в host
|
|
||||||
// В этом случае добавляем порт явно для localhost или если порт указан в URL
|
|
||||||
if (!domain.includes(':')) {
|
if (!domain.includes(':')) {
|
||||||
if (baseUrlObj.port) {
|
// Извлекаем hostname из host (если порта нет, host = hostname)
|
||||||
// Порт есть в URL, но не в host (стандартный порт)
|
const hostname = host;
|
||||||
domain = `${baseUrlObj.hostname}:${baseUrlObj.port}`;
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||||
} else if (baseUrlObj.hostname === 'localhost' || baseUrlObj.hostname === '127.0.0.1') {
|
domain = `${hostname}:9000`;
|
||||||
// Для localhost добавляем порт явно
|
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
|
||||||
const defaultPort = baseUrlObj.protocol === 'https:' ? '443' : '9000';
|
// IP адрес - добавляем порт
|
||||||
domain = `${baseUrlObj.hostname}:${defaultPort}`;
|
domain = `${hostname}:9000`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const origin = baseUrlForResources; // URI для SIWE (полный URL)
|
|
||||||
|
// Формируем origin и baseUrl для resources
|
||||||
|
// ВАЖНО: origin должен совпадать с origin на фронтенде (getOrigin()),
|
||||||
|
// поэтому используем именно domain (с портом), а не host из заголовка
|
||||||
|
const origin = `${protocol}://${domain}`;
|
||||||
|
const baseUrlForResources = origin;
|
||||||
|
|
||||||
// Получаем список документов для подписания и добавляем их в resources
|
// Получаем список документов для подписания и добавляем их в resources
|
||||||
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
const documentTitles = Object.keys(DOCUMENT_CONSENT_MAP);
|
||||||
@@ -222,6 +222,8 @@ router.post('/verify', async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let resources = [`${baseUrlForResources}/api/auth/verify`];
|
let resources = [`${baseUrlForResources}/api/auth/verify`];
|
||||||
|
// Добавляем общую ссылку на страницу опубликованных документов
|
||||||
|
resources.push(`${baseUrlForResources}/content/published`);
|
||||||
if (tableExistsRes.rows[0].exists) {
|
if (tableExistsRes.rows[0].exists) {
|
||||||
const { rows: documents } = await db.getQuery()(`
|
const { rows: documents } = await db.getQuery()(`
|
||||||
SELECT id FROM ${tableName}
|
SELECT id FROM ${tableName}
|
||||||
@@ -236,13 +238,16 @@ router.post('/verify', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем resources для консистентности (должно совпадать с фронтендом)
|
// Сортируем resources для консистентности
|
||||||
resources = resources.sort();
|
resources = resources.sort();
|
||||||
|
|
||||||
// Используем issuedAt из запроса, если он есть, иначе создаем новый
|
// Используем issuedAt из запроса, если он есть, иначе создаем новый
|
||||||
const messageIssuedAt = issuedAt || new Date().toISOString();
|
const messageIssuedAt = issuedAt || new Date().toISOString();
|
||||||
|
|
||||||
const { SiweMessage } = require('siwe');
|
const { SiweMessage } = require('siwe');
|
||||||
|
|
||||||
|
// Реконструируем SIWE-сообщение на бэкенде, НО уже с теми же полями,
|
||||||
|
// что и на фронтенде: domain, origin (uri), resources
|
||||||
const message = new SiweMessage({
|
const message = new SiweMessage({
|
||||||
domain,
|
domain,
|
||||||
address: normalizedAddress,
|
address: normalizedAddress,
|
||||||
@@ -254,20 +259,21 @@ router.post('/verify', async (req, res) => {
|
|||||||
issuedAt: messageIssuedAt,
|
issuedAt: messageIssuedAt,
|
||||||
resources: resources,
|
resources: resources,
|
||||||
});
|
});
|
||||||
|
const messageToVerify = message;
|
||||||
|
|
||||||
const messageToSign = message.prepareMessage();
|
const messageToSign = message.prepareMessage();
|
||||||
|
|
||||||
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
|
logger.info(`[verify] SIWE message for verification: ${messageToSign}`);
|
||||||
logger.info(`[verify] Resources: ${JSON.stringify(resources)}`);
|
logger.info(`[verify] Resources (backend expectation): ${JSON.stringify(resources)}`);
|
||||||
logger.info(`[verify] IssuedAt: ${messageIssuedAt}`);
|
logger.info(`[verify] IssuedAt (backend): ${messageIssuedAt}`);
|
||||||
logger.info(`[verify] Domain: ${domain}, Origin: ${origin}`);
|
logger.info(`[verify] Domain (backend): ${domain}, Origin (backend): ${origin}`);
|
||||||
logger.info(`[verify] Normalized address: ${normalizedAddress}`);
|
logger.info(`[verify] Normalized address from request: ${normalizedAddress}`);
|
||||||
logger.info(`[verify] Request headers origin: ${req.get('origin')}`);
|
logger.info(`[verify] Request headers origin: ${req.get('origin')}`);
|
||||||
logger.info(`[verify] Request headers host: ${req.get('host')}`);
|
logger.info(`[verify] Request headers host: ${req.get('host')}`);
|
||||||
logger.info(`[verify] Request headers referer: ${req.get('referer')}`);
|
logger.info(`[verify] Request headers referer: ${req.get('referer')}`);
|
||||||
|
|
||||||
// Проверяем подпись через SiweMessage.verify() (передаем объект сообщения, а не строку)
|
// Проверяем подпись через SiweMessage.verify()
|
||||||
const isValid = await authService.verifySignature(message, signature, normalizedAddress);
|
const isValid = await authService.verifySignature(messageToVerify, signature, normalizedAddress);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
logger.error(`[verify] Invalid signature for address: ${normalizedAddress}`);
|
logger.error(`[verify] Invalid signature for address: ${normalizedAddress}`);
|
||||||
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
return res.status(401).json({ success: false, error: 'Invalid signature' });
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем подпись через SiweMessage.verify()
|
// Проверяем подпись через SiweMessage.verify()
|
||||||
|
// SiweMessage.verify() уже гарантирует, что адрес из подписи совпадает с адресом в сообщении
|
||||||
const { success, data } = await message.verify({ signature });
|
const { success, data } = await message.verify({ signature });
|
||||||
|
|
||||||
// Логируем для отладки
|
// Логируем для отладки
|
||||||
@@ -63,23 +64,15 @@ class AuthService {
|
|||||||
logger.info(`[verifySignature] Verified address from signature: ${data.address}`);
|
logger.info(`[verifySignature] Verified address from signature: ${data.address}`);
|
||||||
logger.info(`[verifySignature] Address in message: ${message.address}`);
|
logger.info(`[verifySignature] Address in message: ${message.address}`);
|
||||||
logger.info(`[verifySignature] Expected address (from request): ${normalizedAddress}`);
|
logger.info(`[verifySignature] Expected address (from request): ${normalizedAddress}`);
|
||||||
logger.info(`[verifySignature] Signature address matches message address: ${ethers.getAddress(data.address) === ethers.getAddress(message.address)}`);
|
|
||||||
logger.info(`[verifySignature] Signature address matches request address: ${ethers.getAddress(data.address) === normalizedAddress}`);
|
|
||||||
}
|
}
|
||||||
logger.info(`[verifySignature] Signature: ${signature}`);
|
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
logger.error(`[verifySignature] SIWE verification failed. Success: ${success}, Data: ${JSON.stringify(data)}`);
|
logger.error(`[verifySignature] SIWE verification failed. Success: ${success}, Data: ${JSON.stringify(data)}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// КРИТИЧЕСКАЯ ПРОВЕРКА: Адрес из подписи должен совпадать с адресом в сообщении
|
// Проверяем, что адрес из подписи совпадает с адресом из запроса
|
||||||
if (data && message.address && ethers.getAddress(data.address) !== ethers.getAddress(message.address)) {
|
// (SiweMessage.verify() уже проверил соответствие подписи и сообщения)
|
||||||
logger.error(`[verifySignature] КРИТИЧЕСКАЯ ОШИБКА: Адрес из подписи (${data.address}) не совпадает с адресом в сообщении (${message.address})!`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сравниваем нормализованные адреса: адрес из подписи должен совпадать с адресом из запроса
|
|
||||||
const addressesMatch = data && ethers.getAddress(data.address) === normalizedAddress;
|
const addressesMatch = data && ethers.getAddress(data.address) === normalizedAddress;
|
||||||
if (!addressesMatch) {
|
if (!addressesMatch) {
|
||||||
logger.error(`[verifySignature] Адрес из подписи (${data.address}) не совпадает с адресом из запроса (${normalizedAddress})!`);
|
logger.error(`[verifySignature] Адрес из подписи (${data.address}) не совпадает с адресом из запроса (${normalizedAddress})!`);
|
||||||
|
|||||||
@@ -10,149 +10,19 @@
|
|||||||
* GitHub: https://github.com/VC-HB3-Accelerator
|
* GitHub: https://github.com/VC-HB3-Accelerator
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ethers } from 'ethers';
|
// ВАЖНО:
|
||||||
import axios from '../api/axios';
|
// Здесь мы больше не дублируем SIWE-логику.
|
||||||
import { SiweMessage } from 'siwe';
|
// Вся единая и отлаженная реализация находится в `src/utils/wallet.js` (connectWallet),
|
||||||
|
// а этот сервис просто проксирует вызов, чтобы компоненты могли по-прежнему
|
||||||
|
// использовать знакомый API `connectWithWallet`.
|
||||||
|
|
||||||
|
import { connectWallet } from '../utils/wallet';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обёртка над `connectWallet` для совместимости со старыми импортами.
|
||||||
|
* Возвращает объект формата:
|
||||||
|
* { success: boolean, address?: string, userId?: number, error?: string }
|
||||||
|
*/
|
||||||
export async function connectWithWallet() {
|
export async function connectWithWallet() {
|
||||||
// console.log('Starting wallet connection...');
|
return await connectWallet();
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем наличие MetaMask
|
|
||||||
if (!window.ethereum) {
|
|
||||||
throw new Error('MetaMask not detected. Please install MetaMask.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('MetaMask detected, requesting accounts...');
|
|
||||||
|
|
||||||
// Запрашиваем доступ к аккаунтам
|
|
||||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
|
||||||
|
|
||||||
// console.log('Got accounts:', accounts);
|
|
||||||
|
|
||||||
if (!accounts || accounts.length === 0) {
|
|
||||||
throw new Error('No accounts found. Please unlock MetaMask.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Берем первый аккаунт
|
|
||||||
const address = ethers.getAddress(accounts[0]);
|
|
||||||
// console.log('Normalized address:', address);
|
|
||||||
|
|
||||||
// Запрашиваем nonce с сервера
|
|
||||||
// console.log('Requesting nonce...');
|
|
||||||
const nonceResponse = await axios.get(`/auth/nonce?address=${address}`);
|
|
||||||
const nonce = nonceResponse.data.nonce;
|
|
||||||
// console.log('Got nonce:', nonce);
|
|
||||||
|
|
||||||
if (!nonce) {
|
|
||||||
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.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.';
|
|
||||||
|
|
||||||
const issuedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
// Создаем копию resources и сортируем (не мутируем исходный массив)
|
|
||||||
const sortedResources = [...resources].sort();
|
|
||||||
|
|
||||||
const siweMessage = new SiweMessage({
|
|
||||||
domain,
|
|
||||||
address,
|
|
||||||
statement,
|
|
||||||
uri: origin,
|
|
||||||
version: '1',
|
|
||||||
chainId: 1,
|
|
||||||
nonce,
|
|
||||||
issuedAt,
|
|
||||||
resources: sortedResources,
|
|
||||||
});
|
|
||||||
|
|
||||||
const message = siweMessage.prepareMessage();
|
|
||||||
// console.log('SIWE message:', message);
|
|
||||||
// console.log('SIWE message details:', {
|
|
||||||
// domain,
|
|
||||||
// address,
|
|
||||||
// statement,
|
|
||||||
// uri: origin,
|
|
||||||
// version: '1',
|
|
||||||
// chainId: 1,
|
|
||||||
// nonce,
|
|
||||||
// issuedAt,
|
|
||||||
// resources: [`${origin}/auth/verify`],
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Запрашиваем подпись
|
|
||||||
// console.log('Requesting signature...');
|
|
||||||
const signature = await window.ethereum.request({
|
|
||||||
method: 'personal_sign',
|
|
||||||
params: [message, address.toLowerCase()],
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log('Got signature:', signature);
|
|
||||||
|
|
||||||
// Отправляем подпись на сервер для верификации
|
|
||||||
// console.log('Sending verification request...');
|
|
||||||
const verificationResponse = await axios.post('/auth/verify', {
|
|
||||||
signature,
|
|
||||||
address,
|
|
||||||
nonce,
|
|
||||||
issuedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log('Verification response:', verificationResponse.data);
|
|
||||||
|
|
||||||
// Обновляем состояние аутентификации
|
|
||||||
if (verificationResponse.data.success) {
|
|
||||||
// Обновляем состояние аутентификации в localStorage
|
|
||||||
localStorage.setItem('isAuthenticated', 'true');
|
|
||||||
localStorage.setItem('userId', verificationResponse.data.userId);
|
|
||||||
localStorage.setItem('address', verificationResponse.data.address);
|
|
||||||
}
|
|
||||||
|
|
||||||
return verificationResponse.data;
|
|
||||||
} catch (error) {
|
|
||||||
// console.error('Error connecting wallet:', error);
|
|
||||||
|
|
||||||
// Улучшенная обработка ошибок MetaMask
|
|
||||||
let errorMessage = 'Произошла ошибка при подключении кошелька.';
|
|
||||||
|
|
||||||
if (error.message && error.message.includes('MetaMask extension not found')) {
|
|
||||||
errorMessage = 'Расширение MetaMask не найдено. Пожалуйста, установите MetaMask и обновите страницу.';
|
|
||||||
} else if (error.message && error.message.includes('Failed to connect to MetaMask')) {
|
|
||||||
errorMessage = 'Не удалось подключиться к MetaMask. Проверьте, что расширение установлено и активно.';
|
|
||||||
} else if (error.code === 4001) {
|
|
||||||
errorMessage = 'Вы отклонили запрос на подключение в MetaMask.';
|
|
||||||
} else if (error.message && error.message.includes('No accounts found')) {
|
|
||||||
errorMessage = 'Аккаунты не найдены. Пожалуйста, разблокируйте MetaMask и попробуйте снова.';
|
|
||||||
} else if (error.message && error.message.includes('MetaMask not detected')) {
|
|
||||||
errorMessage = 'MetaMask не обнаружен. Пожалуйста, установите расширение MetaMask.';
|
|
||||||
} else if (error.response && error.response.data && error.response.data.error) {
|
|
||||||
errorMessage = error.response.data.error;
|
|
||||||
} else if (error.message) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Возвращаем объект с ошибкой вместо выброса исключения
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: errorMessage
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,30 +10,85 @@
|
|||||||
* GitHub: https://github.com/VC-HB3-Accelerator
|
* GitHub: https://github.com/VC-HB3-Accelerator
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from 'axios';
|
// ВАЖНО: используем общий axios-инстанс с baseURL `/api`,
|
||||||
|
// чтобы все запросы шли через один и тот же API-слой
|
||||||
|
import api from '../api/axios';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { SiweMessage } from 'siwe';
|
import { SiweMessage } from 'siwe';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нормализует Ethereum адрес
|
||||||
|
*/
|
||||||
|
const normalizeAddress = (address) => {
|
||||||
|
return ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает актуальный адрес из кошелька
|
||||||
|
* ВАЖНО: используем ethereum.selectedAddress, т.к. некоторые кошельки
|
||||||
|
* могут подписывать сообщением активным аккаунтом, игнорируя список eth_accounts.
|
||||||
|
* Если selectedAddress недоступен, падаем обратно на eth_accounts.
|
||||||
|
*/
|
||||||
|
const getCurrentAddress = async () => {
|
||||||
|
if (!window.ethereum) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawAddress = null;
|
||||||
|
|
||||||
|
// 1. Пробуем взять текущий активный аккаунт
|
||||||
|
if (window.ethereum.selectedAddress) {
|
||||||
|
rawAddress = window.ethereum.selectedAddress;
|
||||||
|
} else {
|
||||||
|
// 2. Фоллбек на eth_accounts
|
||||||
|
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
|
||||||
|
if (!accounts || accounts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
rawAddress = accounts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeAddress(rawAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формирует domain для SIWE сообщения (должен совпадать с бэкендом)
|
||||||
|
*/
|
||||||
|
const getDomain = () => {
|
||||||
|
let domain = window.location.host;
|
||||||
|
// Если порта нет, добавляем его для localhost или IP адресов
|
||||||
|
if (!domain.includes(':')) {
|
||||||
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||||
|
domain = `${window.location.hostname}:${window.location.port || '9000'}`;
|
||||||
|
} else if (/^\d+\.\d+\.\d+\.\d+$/.test(window.location.hostname)) {
|
||||||
|
domain = `${window.location.hostname}:${window.location.port || '9000'}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domain;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Формирует origin для SIWE сообщения (должен совпадать с бэкендом)
|
||||||
|
* window.location.origin может не включать порт, поэтому формируем явно
|
||||||
|
*/
|
||||||
|
const getOrigin = () => {
|
||||||
|
const protocol = window.location.protocol; // Уже содержит ':' (например, 'http:')
|
||||||
|
const domain = getDomain(); // Используем domain, который уже содержит порт
|
||||||
|
return `${protocol}//${domain}`; // Двойной слеш после протокола (http:// или https://)
|
||||||
|
};
|
||||||
|
|
||||||
export const connectWallet = async () => {
|
export const connectWallet = async () => {
|
||||||
try {
|
try {
|
||||||
// console.log('Starting wallet connection...');
|
// Проверяем наличие MetaMask
|
||||||
|
|
||||||
// Проверяем наличие MetaMask или другого Ethereum провайдера
|
|
||||||
if (!window.ethereum) {
|
if (!window.ethereum) {
|
||||||
// console.error('No Ethereum provider (like MetaMask) detected!');
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: 'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
|
||||||
'Не найден кошелек MetaMask или другой Ethereum провайдер. Пожалуйста, установите расширение MetaMask.',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('MetaMask detected, requesting accounts...');
|
|
||||||
|
|
||||||
// Запрашиваем доступ к аккаунтам
|
// Запрашиваем доступ к аккаунтам
|
||||||
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
// console.log('Got accounts:', accounts);
|
|
||||||
|
|
||||||
if (!accounts || accounts.length === 0) {
|
if (!accounts || accounts.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -41,36 +96,43 @@ export const connectWallet = async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Берем первый аккаунт в списке
|
// КРИТИЧЕСКИ ВАЖНО: Получаем актуальный адрес ОДИН РАЗ и используем его везде
|
||||||
const address = accounts[0];
|
// Это гарантирует, что весь процесс (nonce, сообщение, подпись) использует один и тот же адрес
|
||||||
// Нормализуем адрес (используем getAddress для совместимости)
|
const walletAddress = await getCurrentAddress();
|
||||||
// Проверяем версию ethers - если v6, используем ethers.getAddress, иначе ethers.utils.getAddress
|
if (!walletAddress) {
|
||||||
const normalizedAddress = ethers.getAddress ? ethers.getAddress(address) : ethers.utils.getAddress(address);
|
|
||||||
// console.log('Normalized address:', normalizedAddress);
|
|
||||||
|
|
||||||
// ВАЖНО: Получаем актуальный адрес ПЕРЕД запросом nonce, чтобы избежать проблем с переключением кошелька
|
|
||||||
const currentAccounts = await window.ethereum.request({ method: 'eth_accounts' });
|
|
||||||
if (!currentAccounts || currentAccounts.length === 0) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Кошелек не подключен. Пожалуйста, подключите кошелек и попробуйте снова.',
|
error: 'Кошелек не подключен. Пожалуйста, подключите кошелек и попробуйте снова.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Используем актуальный адрес из кошелька
|
// Формируем domain и origin (должны совпадать с бэкендом)
|
||||||
const currentAddress = ethers.getAddress ? ethers.getAddress(currentAccounts[0]) : ethers.utils.getAddress(currentAccounts[0]);
|
const domain = getDomain();
|
||||||
|
const origin = getOrigin();
|
||||||
|
|
||||||
// Проверяем, что адрес совпадает с изначальным
|
// Получаем список документов для подписания
|
||||||
if (ethers.getAddress(currentAddress) !== ethers.getAddress(normalizedAddress)) {
|
// ВАЖНО: Пути должны точно совпадать с бэкендом для успешной верификации SIWE
|
||||||
console.warn('⚠️ [Frontend] Адрес кошелька изменился с момента подключения! Используем актуальный адрес:', currentAddress);
|
let resources = [`${origin}/api/auth/verify`];
|
||||||
|
// Добавляем общую ссылку на страницу опубликованных документов
|
||||||
|
resources.push(`${origin}/content/published`);
|
||||||
|
try {
|
||||||
|
const docsResponse = await api.get('/consent/documents');
|
||||||
|
if (docsResponse.data && docsResponse.data.length > 0) {
|
||||||
|
docsResponse.data.forEach(doc => {
|
||||||
|
// Используем тот же путь, что и на бэкенде: /content/published/${doc.id}
|
||||||
|
resources.push(`${origin}/content/published/${doc.id}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Не удалось получить список документов для подписания:', error);
|
||||||
|
}
|
||||||
|
const issuedAt = new Date().toISOString();
|
||||||
|
const sortedResources = [...resources].sort();
|
||||||
|
|
||||||
// Запрашиваем nonce с сервера для АКТУАЛЬНОГО адреса
|
// Запрашиваем nonce для адреса кошелька
|
||||||
// console.log('Requesting nonce...');
|
// ВАЖНО: Бэкенд сохраняет nonce для address.toLowerCase(), поэтому отправляем адрес в нижнем регистре
|
||||||
const nonceResponse = await axios.get(`/auth/nonce?address=${currentAddress}`);
|
const nonceResponse = await api.get(`/auth/nonce?address=${walletAddress.toLowerCase()}`);
|
||||||
const nonce = nonceResponse.data.nonce;
|
const nonce = nonceResponse.data.nonce;
|
||||||
// console.log('Got nonce:', nonce);
|
|
||||||
|
|
||||||
if (!nonce) {
|
if (!nonce) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -78,108 +140,87 @@ export const connectWallet = async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для SIWE используем personal_sign напрямую через window.ethereum
|
// КРИТИЧЕСКАЯ ПРОВЕРКА: Убеждаемся, что адрес не изменился перед подписанием
|
||||||
// Не используем ethers signer, так как он добавляет префикс, который нарушает SIWE формат
|
// personal_sign всегда использует текущий активный аккаунт, поэтому адрес в сообщении
|
||||||
|
// должен совпадать с адресом, который будет подписывать
|
||||||
// Получаем список документов для подписания
|
const addressBeforeSign = await getCurrentAddress();
|
||||||
let resources = [`${window.location.origin}/api/auth/verify`];
|
if (!addressBeforeSign) {
|
||||||
try {
|
|
||||||
const docsResponse = await axios.get('/consent/documents');
|
|
||||||
if (docsResponse.data && docsResponse.data.length > 0) {
|
|
||||||
docsResponse.data.forEach(doc => {
|
|
||||||
resources.push(`${window.location.origin}/public/page/${doc.id}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Если не удалось получить документы, продолжаем без них
|
|
||||||
console.warn('Не удалось получить список документов для подписания:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем сообщение для подписи
|
|
||||||
// Важно: 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();
|
|
||||||
|
|
||||||
// Создаем копию resources и сортируем (не мутируем исходный массив)
|
|
||||||
const sortedResources = [...resources].sort();
|
|
||||||
|
|
||||||
// КРИТИЧЕСКАЯ ПРОВЕРКА: Получаем актуальный адрес ПЕРЕД созданием сообщения
|
|
||||||
const accountsBeforeMessage = await window.ethereum.request({ method: 'eth_accounts' });
|
|
||||||
if (!accountsBeforeMessage || accountsBeforeMessage.length === 0) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Кошелек отключен. Пожалуйста, подключите кошелек и попробуйте снова.',
|
error: 'Не удалось получить адрес кошелька. Пожалуйста, попробуйте снова.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const addressForMessage = ethers.getAddress ? ethers.getAddress(accountsBeforeMessage[0]) : ethers.utils.getAddress(accountsBeforeMessage[0]);
|
// Нормализуем адрес для использования в сообщении
|
||||||
|
// ВАЖНО: SiweMessage может нормализовать адрес, поэтому нормализуем его заранее
|
||||||
|
const normalizedAddressForMessage = normalizeAddress(addressBeforeSign);
|
||||||
|
|
||||||
// Проверяем, что адрес не изменился
|
// Проверяем, что адрес не изменился
|
||||||
if (ethers.getAddress(addressForMessage) !== ethers.getAddress(currentAddress)) {
|
const normalizedWalletAddress = normalizeAddress(walletAddress);
|
||||||
console.warn('⚠️ [Frontend] Адрес изменился перед созданием сообщения! Используем актуальный адрес:', addressForMessage);
|
if (normalizedAddressForMessage !== normalizedWalletAddress) {
|
||||||
|
console.error('❌ [Frontend] Адрес изменился перед подписанием!');
|
||||||
|
console.error(' Ожидался (нормализован):', normalizedWalletAddress);
|
||||||
|
console.error(' Получен (нормализован):', normalizedAddressForMessage);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Адрес кошелька изменился. Пожалуйста, попробуйте снова.',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаем SIWE сообщение с документами в resources, используя АКТУАЛЬНЫЙ адрес
|
// Создаем SIWE сообщение с нормализованным адресом
|
||||||
|
// ВАЖНО: адрес в сообщении должен совпадать с адресом, который подписывает
|
||||||
const message = new SiweMessage({
|
const message = new SiweMessage({
|
||||||
domain,
|
domain,
|
||||||
address: addressForMessage, // Используем актуальный адрес из кошелька ПЕРЕД созданием сообщения
|
address: normalizedAddressForMessage, // Используем нормализованный адрес
|
||||||
statement: 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.',
|
statement: 'Sign in with Ethereum to the app.\n\nПодписывая это сообщение, вы подтверждаете ознакомление с документами, указанными в Resources, и согласие на обработку персональных данных.',
|
||||||
uri: origin,
|
uri: origin,
|
||||||
version: '1',
|
version: '1',
|
||||||
chainId: 1, // Ethereum mainnet
|
chainId: 1,
|
||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
issuedAt: issuedAt,
|
issuedAt: issuedAt,
|
||||||
resources: sortedResources,
|
resources: sortedResources,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Получаем строку сообщения для подписи
|
|
||||||
const messageToSign = message.prepareMessage();
|
const messageToSign = message.prepareMessage();
|
||||||
|
|
||||||
// КРИТИЧЕСКАЯ ПРОВЕРКА: Получаем актуальный адрес ПЕРЕД подписанием
|
// КРИТИЧЕСКАЯ ПРОВЕРКА: Убеждаемся, что адрес в сообщении совпадает с адресом, который будет подписывать
|
||||||
const accountsBeforeSign = await window.ethereum.request({ method: 'eth_accounts' });
|
// SiweMessage может нормализовать адрес, поэтому проверяем после создания сообщения
|
||||||
if (!accountsBeforeSign || accountsBeforeSign.length === 0) {
|
const messageAddress = message.address;
|
||||||
|
const normalizedMessageAddress = normalizeAddress(messageAddress);
|
||||||
|
const normalizedSignAddress = normalizeAddress(addressBeforeSign);
|
||||||
|
|
||||||
|
if (normalizedMessageAddress !== normalizedSignAddress) {
|
||||||
|
console.error('❌ [Frontend] КРИТИЧЕСКАЯ ОШИБКА: Адрес в сообщении не совпадает с адресом для подписи!');
|
||||||
|
console.error(' Адрес в сообщении (исходный):', messageAddress);
|
||||||
|
console.error(' Адрес в сообщении (нормализован):', normalizedMessageAddress);
|
||||||
|
console.error(' Адрес для подписи (исходный):', addressBeforeSign);
|
||||||
|
console.error(' Адрес для подписи (нормализован):', normalizedSignAddress);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Кошелек отключен перед подписанием. Пожалуйста, подключите кошелек и попробуйте снова.',
|
error: 'Несоответствие адресов в сообщении и подписи. Пожалуйста, попробуйте снова.',
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const addressForSign = ethers.getAddress ? ethers.getAddress(accountsBeforeSign[0]) : ethers.utils.getAddress(accountsBeforeSign[0]);
|
|
||||||
|
|
||||||
// Проверяем, что адрес для подписи совпадает с адресом в сообщении
|
|
||||||
if (ethers.getAddress(addressForSign) !== ethers.getAddress(addressForMessage)) {
|
|
||||||
console.error('❌ [Frontend] КРИТИЧЕСКАЯ ОШИБКА: Адрес для подписи не совпадает с адресом в сообщении!');
|
|
||||||
console.error(' Адрес в сообщении:', addressForMessage);
|
|
||||||
console.error(' Адрес для подписи:', addressForSign);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес кошелька изменился перед подписанием. Пожалуйста, попробуйте снова.',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Логируем для отладки
|
// Логируем для отладки
|
||||||
console.log('🔐 [Frontend] Domain:', domain);
|
console.log('🔐 [Frontend] Domain:', domain);
|
||||||
console.log('🔐 [Frontend] Origin:', origin);
|
console.log('🔐 [Frontend] Origin:', origin);
|
||||||
console.log('🔐 [Frontend] Address:', currentAddress);
|
console.log('🔐 [Frontend] Address in message (original):', messageAddress);
|
||||||
|
console.log('🔐 [Frontend] Address in message (normalized):', normalizedMessageAddress);
|
||||||
|
console.log('🔐 [Frontend] Address for sign (original):', addressBeforeSign);
|
||||||
|
console.log('🔐 [Frontend] Address for sign (normalized):', normalizedSignAddress);
|
||||||
|
console.log('🔐 [Frontend] Addresses match (normalized):', normalizedMessageAddress === normalizedSignAddress);
|
||||||
console.log('🔐 [Frontend] Nonce:', nonce);
|
console.log('🔐 [Frontend] Nonce:', nonce);
|
||||||
console.log('🔐 [Frontend] IssuedAt:', issuedAt);
|
console.log('🔐 [Frontend] IssuedAt:', issuedAt);
|
||||||
console.log('🔐 [Frontend] Resources:', JSON.stringify(sortedResources));
|
console.log('🔐 [Frontend] Resources:', JSON.stringify(sortedResources));
|
||||||
console.log('🔐 [Frontend] SIWE message to sign:', messageToSign);
|
console.log('🔐 [Frontend] SIWE message to sign:', messageToSign);
|
||||||
console.log('🔐 [Frontend] Message length:', messageToSign.length);
|
|
||||||
|
|
||||||
// Запрашиваем подпись через personal_sign (правильный способ для SIWE)
|
// Запрашиваем подпись
|
||||||
// personal_sign подписывает сообщение С префиксом "\x19Ethereum Signed Message:\n"
|
// ВАЖНО: personal_sign может игнорировать второй параметр (адрес) и использовать текущий активный аккаунт
|
||||||
// ethers.verifyMessage() также добавляет этот префикс, поэтому они совместимы
|
// Поэтому мы должны убедиться, что адрес в сообщении совпадает с адресом, который будет подписывать
|
||||||
// Параметры: [message, address] - MetaMask принимает строку напрямую
|
// Используем нормализованный адрес для подписи
|
||||||
// ВАЖНО: Используем addressForSign, чтобы подпись была от актуального кошелька
|
|
||||||
const signature = await window.ethereum.request({
|
const signature = await window.ethereum.request({
|
||||||
method: 'personal_sign',
|
method: 'personal_sign',
|
||||||
params: [messageToSign, addressForSign.toLowerCase()],
|
params: [messageToSign, normalizedMessageAddress.toLowerCase()], // Используем нормализованный адрес из сообщения
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
@@ -189,80 +230,38 @@ export const connectWallet = async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('Got signature:', signature);
|
|
||||||
|
|
||||||
// КРИТИЧЕСКАЯ ПРОВЕРКА: Убеждаемся, что адрес не изменился после подписи
|
|
||||||
const finalAccounts = await window.ethereum.request({ method: 'eth_accounts' });
|
|
||||||
if (!finalAccounts || finalAccounts.length === 0) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Кошелек отключен. Пожалуйста, подключите кошелек и попробуйте снова.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalAddress = ethers.getAddress ? ethers.getAddress(finalAccounts[0]) : ethers.utils.getAddress(finalAccounts[0]);
|
|
||||||
|
|
||||||
// Проверяем, что адрес совпадает с тем, который использовался для подписи
|
|
||||||
if (ethers.getAddress(finalAddress) !== ethers.getAddress(addressForSign)) {
|
|
||||||
console.error('❌ [Frontend] КРИТИЧЕСКАЯ ОШИБКА: Адрес кошелька изменился после подписи!');
|
|
||||||
console.error(' Адрес при подписи:', addressForSign);
|
|
||||||
console.error(' Текущий адрес:', finalAddress);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Адрес кошелька изменился после подписи. Пожалуйста, попробуйте снова.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проверяем, что адрес в сообщении совпадает с адресом, который подписывает
|
|
||||||
const messageAddress = message.address;
|
|
||||||
if (ethers.getAddress(messageAddress) !== ethers.getAddress(addressForSign)) {
|
|
||||||
console.error('❌ [Frontend] КРИТИЧЕСКАЯ ОШИБКА: Адрес в сообщении не совпадает с адресом подписи!');
|
|
||||||
console.error(' Адрес в сообщении:', messageAddress);
|
|
||||||
console.error(' Адрес подписи:', addressForSign);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: 'Несоответствие адресов в сообщении и подписи. Пожалуйста, попробуйте снова.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Отправляем верификацию на сервер
|
// Отправляем верификацию на сервер
|
||||||
// console.log('Sending verification request...');
|
// Используем нормализованный адрес из сообщения (должен совпадать с адресом, который подписал)
|
||||||
const requestData = {
|
const requestData = {
|
||||||
address: addressForSign, // Используем адрес, который подписал сообщение
|
address: normalizedMessageAddress, // Нормализованный адрес из сообщения (должен совпадать с адресом подписи)
|
||||||
signature,
|
signature,
|
||||||
nonce,
|
nonce,
|
||||||
issuedAt: issuedAt, // Используем тот же issuedAt, что и в сообщении
|
issuedAt: issuedAt,
|
||||||
};
|
};
|
||||||
// console.log('Request data:', requestData);
|
|
||||||
|
|
||||||
const verifyResponse = await axios.post('/auth/verify', requestData, {
|
const verifyResponse = await api.post('/auth/verify', requestData, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновляем интерфейс для отображения подключенного состояния
|
// Обновляем интерфейс
|
||||||
document.body.classList.add('wallet-connected');
|
document.body.classList.add('wallet-connected');
|
||||||
|
|
||||||
// Обновляем отображение адреса кошелька в UI
|
|
||||||
const authDisplayEl = document.getElementById('auth-display');
|
const authDisplayEl = document.getElementById('auth-display');
|
||||||
if (authDisplayEl) {
|
if (authDisplayEl) {
|
||||||
const shortAddress = `${normalizedAddress.substring(0, 6)}...${normalizedAddress.substring(normalizedAddress.length - 4)}`;
|
const shortAddress = `${walletAddress.substring(0, 6)}...${walletAddress.substring(walletAddress.length - 4)}`;
|
||||||
authDisplayEl.innerHTML = `Кошелек: <strong>${shortAddress}</strong>`;
|
authDisplayEl.innerHTML = `Кошелек: <strong>${shortAddress}</strong>`;
|
||||||
authDisplayEl.style.display = 'inline-block';
|
authDisplayEl.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скрываем кнопки авторизации и показываем кнопку выхода
|
|
||||||
const authButtonsEl = document.getElementById('auth-buttons');
|
const authButtonsEl = document.getElementById('auth-buttons');
|
||||||
const logoutButtonEl = document.getElementById('logout-button');
|
const logoutButtonEl = document.getElementById('logout-button');
|
||||||
|
|
||||||
if (authButtonsEl) authButtonsEl.style.display = 'none';
|
if (authButtonsEl) authButtonsEl.style.display = 'none';
|
||||||
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
|
if (logoutButtonEl) logoutButtonEl.style.display = 'inline-block';
|
||||||
|
|
||||||
// console.log('Verification response:', verifyResponse.data);
|
|
||||||
|
|
||||||
if (verifyResponse.data.success) {
|
if (verifyResponse.data.success) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
address: normalizedAddress,
|
address: walletAddress,
|
||||||
userId: verifyResponse.data.userId,
|
userId: verifyResponse.data.userId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -272,9 +271,6 @@ export const connectWallet = async () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error('Error connecting wallet:', error);
|
|
||||||
|
|
||||||
// Формируем понятное сообщение об ошибке
|
|
||||||
let errorMessage = 'Произошла ошибка при подключении кошелька.';
|
let errorMessage = 'Произошла ошибка при подключении кошелька.';
|
||||||
|
|
||||||
if (error.message && error.message.includes('MetaMask extension not found')) {
|
if (error.message && error.message.includes('MetaMask extension not found')) {
|
||||||
|
|||||||
Reference in New Issue
Block a user