Add complete Web3-powered token authentication module for SecureBit project - **TokenAuthManager.js**: Main authentication manager handling Web3 wallet connections, session creation/validation, and automatic session termination - **Web3ContractManager.js**: Smart contract interface for token operations and validation - **SecureBitAccessToken.sol**: ERC-721 smart contract for access tokens with monthly/yearly durations - **TokenAuthModal.jsx**: User interface for wallet connection and token purchase - **TokenStatus.jsx**: Header component displaying token status and remaining time - ERC-721 compliant access tokens with configurable durations (1 month/1 year) - OpenZeppelin security contracts integration (Ownable, ReentrancyGuard, Pausable) - Token purchase, renewal, and deactivation functionality - Automatic expiry validation and price management - Transfer handling with user token tracking - Pausable functionality for emergency contract control - `purchaseMonthlyToken()` / `purchaseYearlyToken()`: Token acquisition - `isTokenValid()`: Real-time token validation - `renewToken()`: Token extension functionality - `deactivateToken()`: Manual token deactivation - `getTokenPrices()`: Dynamic pricing information - `pause()` / `unpause()`: Emergency control functions - Web3 signature verification for wallet ownership - Single active session enforcement per account - Automatic session termination on new device login - Cryptographic signature validation - MITM and replay attack protection preservation - Blockchain-based token validation - Modular architecture for easy integration - Web3.js integration for Ethereum network interaction - MetaMask wallet support - Session heartbeat monitoring - Automatic token expiry handling - Comprehensive error handling and logging - src/token-auth/TokenAuthManager.js - src/token-auth/Web3ContractManager.js - src/token-auth/SecureBitAccessToken.sol - src/token-auth/config.js - src/components/ui/TokenAuthModal.jsx - src/components/ui/TokenStatus.jsx - Smart contract includes comprehensive test scenarios - Mock mode available for development testing - Hardhat deployment scripts provided
622 lines
22 KiB
JavaScript
622 lines
22 KiB
JavaScript
// ============================================
|
||
// WEB3 CONTRACT MANAGER
|
||
// ============================================
|
||
// Управление смарт-контрактом токенов доступа
|
||
// Интеграция с MetaMask и другими Web3 провайдерами
|
||
// ============================================
|
||
|
||
class Web3ContractManager {
|
||
constructor() {
|
||
this.contract = null;
|
||
this.web3 = null;
|
||
this.contractAddress = null;
|
||
this.contractABI = null;
|
||
this.isInitialized = false;
|
||
|
||
// Адреса контрактов для разных сетей
|
||
this.CONTRACT_ADDRESSES = {
|
||
// Mainnet
|
||
'0x1': '0x0000000000000000000000000000000000000000', // Заменить на реальный адрес
|
||
// Ropsten (тестовая сеть)
|
||
'0x3': '0x0000000000000000000000000000000000000000', // Заменить на реальный адрес
|
||
// Goerli (тестовая сеть)
|
||
'0x5': '0x0000000000000000000000000000000000000000', // Заменить на реальный адрес
|
||
// Sepolia (тестовая сеть)
|
||
'0xaa36a7': '0x0000000000000000000000000000000000000000' // Заменить на реальный адрес
|
||
};
|
||
|
||
// ABI контракта (упрощенная версия)
|
||
this.CONTRACT_ABI = [
|
||
// События
|
||
{
|
||
"anonymous": false,
|
||
"inputs": [
|
||
{"indexed": true, "name": "tokenId", "type": "uint256"},
|
||
{"indexed": true, "name": "owner", "type": "address"},
|
||
{"indexed": false, "name": "tokenType", "type": "uint8"},
|
||
{"indexed": false, "name": "expiryDate", "type": "uint256"}
|
||
],
|
||
"name": "TokenMinted",
|
||
"type": "event"
|
||
},
|
||
{
|
||
"anonymous": false,
|
||
"inputs": [
|
||
{"indexed": true, "name": "tokenId", "type": "uint256"},
|
||
{"indexed": true, "name": "owner", "type": "address"}
|
||
],
|
||
"name": "TokenExpired",
|
||
"type": "event"
|
||
},
|
||
{
|
||
"anonymous": false,
|
||
"inputs": [
|
||
{"indexed": true, "name": "tokenId", "type": "uint256"},
|
||
{"indexed": false, "name": "newExpiryDate", "type": "uint256"}
|
||
],
|
||
"name": "TokenRenewed",
|
||
"type": "event"
|
||
},
|
||
|
||
// Функции чтения
|
||
{
|
||
"inputs": [{"name": "tokenId", "type": "uint256"}],
|
||
"name": "isTokenValid",
|
||
"outputs": [{"name": "", "type": "bool"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "tokenId", "type": "uint256"}],
|
||
"name": "getTokenInfo",
|
||
"outputs": [
|
||
{"name": "tokenId", "type": "uint256"},
|
||
{"name": "owner", "type": "address"},
|
||
{"name": "expiryDate", "type": "uint256"},
|
||
{"name": "tokenType", "type": "uint8"},
|
||
{"name": "isActive", "type": "bool"},
|
||
{"name": "createdAt", "type": "uint256"},
|
||
{"name": "metadata", "type": "string"}
|
||
],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "user", "type": "address"}],
|
||
"name": "getUserTokens",
|
||
"outputs": [{"name": "", "type": "uint256[]"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "user", "type": "address"}],
|
||
"name": "getActiveUserTokens",
|
||
"outputs": [{"name": "", "type": "uint256[]"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "user", "type": "address"}],
|
||
"name": "hasActiveToken",
|
||
"outputs": [{"name": "", "type": "bool"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "monthlyPrice",
|
||
"outputs": [{"name": "", "type": "uint256"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "yearlyPrice",
|
||
"outputs": [{"name": "", "type": "uint256"}],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "getStats",
|
||
"outputs": [
|
||
{"name": "totalTokens", "type": "uint256"},
|
||
{"name": "activeTokens", "type": "uint256"},
|
||
{"name": "monthlyTokens", "type": "uint256"},
|
||
{"name": "yearlyTokens", "type": "uint256"}
|
||
],
|
||
"stateMutability": "view",
|
||
"type": "function"
|
||
},
|
||
|
||
// Функции записи
|
||
{
|
||
"inputs": [],
|
||
"name": "purchaseMonthlyToken",
|
||
"outputs": [],
|
||
"stateMutability": "payable",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [],
|
||
"name": "purchaseYearlyToken",
|
||
"outputs": [],
|
||
"stateMutability": "payable",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "tokenId", "type": "uint256"}],
|
||
"name": "deactivateToken",
|
||
"outputs": [],
|
||
"stateMutability": "nonpayable",
|
||
"type": "function"
|
||
},
|
||
{
|
||
"inputs": [{"name": "tokenId", "type": "uint256"}],
|
||
"name": "renewToken",
|
||
"outputs": [],
|
||
"stateMutability": "payable",
|
||
"type": "function"
|
||
}
|
||
];
|
||
|
||
this.initialize();
|
||
}
|
||
|
||
// Инициализация Web3 и контракта
|
||
async initialize() {
|
||
try {
|
||
// Проверяем поддержку Web3
|
||
if (typeof window.ethereum !== 'undefined') {
|
||
console.log('✅ Web3 detected');
|
||
|
||
// Создаем Web3 экземпляр
|
||
this.web3 = new Web3(window.ethereum);
|
||
|
||
// Получаем текущую сеть
|
||
const chainId = await this.getCurrentChainId();
|
||
console.log('🔗 Current chain ID:', chainId);
|
||
|
||
// Получаем адрес контракта для текущей сети
|
||
this.contractAddress = this.CONTRACT_ADDRESSES[chainId];
|
||
|
||
if (!this.contractAddress || this.contractAddress === '0x0000000000000000000000000000000000000000') {
|
||
console.warn('⚠️ Contract not deployed on current network:', chainId);
|
||
this.showContractNotDeployedMessage(chainId);
|
||
return;
|
||
}
|
||
|
||
// Создаем экземпляр контракта
|
||
this.contract = new this.web3.eth.Contract(
|
||
this.CONTRACT_ABI,
|
||
this.contractAddress
|
||
);
|
||
|
||
console.log('📋 Contract initialized:', this.contractAddress);
|
||
this.isInitialized = true;
|
||
|
||
} else {
|
||
throw new Error('Web3 not detected');
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Web3ContractManager initialization failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение текущего Chain ID
|
||
async getCurrentChainId() {
|
||
try {
|
||
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
|
||
return chainId;
|
||
} catch (error) {
|
||
console.error('❌ Failed to get chain ID:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Проверка валидности токена
|
||
async isTokenValid(tokenId) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.isTokenValid(tokenId).call();
|
||
console.log('🔍 Token validation result:', { tokenId, isValid: result });
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Token validation failed:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Получение информации о токене
|
||
async getTokenInfo(tokenId) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.getTokenInfo(tokenId).call();
|
||
|
||
// Преобразуем результат в удобный формат
|
||
const tokenInfo = {
|
||
tokenId: result.tokenId,
|
||
owner: result.owner,
|
||
expiryDate: parseInt(result.expiryDate),
|
||
tokenType: parseInt(result.tokenType),
|
||
isActive: result.isActive,
|
||
createdAt: parseInt(result.createdAt),
|
||
metadata: result.metadata
|
||
};
|
||
|
||
console.log('📋 Token info retrieved:', tokenInfo);
|
||
return tokenInfo;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get token info:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение токенов пользователя
|
||
async getUserTokens(userAddress) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.getUserTokens(userAddress).call();
|
||
const tokenIds = result.map(id => parseInt(id));
|
||
|
||
console.log('👤 User tokens retrieved:', { user: userAddress, tokens: tokenIds });
|
||
return tokenIds;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get user tokens:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение активных токенов пользователя
|
||
async getActiveUserTokens(userAddress) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.getActiveUserTokens(userAddress).call();
|
||
const tokenIds = result.map(id => parseInt(id));
|
||
|
||
console.log('✅ Active user tokens retrieved:', { user: userAddress, activeTokens: tokenIds });
|
||
return tokenIds;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get active user tokens:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Проверка наличия активного токена у пользователя
|
||
async hasActiveToken(userAddress) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.hasActiveToken(userAddress).call();
|
||
console.log('🔍 Active token check:', { user: userAddress, hasActive: result });
|
||
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to check active token:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Получение цен токенов
|
||
async getTokenPrices() {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const [monthlyPrice, yearlyPrice] = await Promise.all([
|
||
this.contract.methods.monthlyPrice().call(),
|
||
this.contract.methods.yearlyPrice().call()
|
||
]);
|
||
|
||
const prices = {
|
||
monthly: this.web3.utils.fromWei(monthlyPrice, 'ether'),
|
||
yearly: this.web3.utils.fromWei(yearlyPrice, 'ether'),
|
||
monthlyWei: monthlyPrice,
|
||
yearlyWei: yearlyPrice
|
||
};
|
||
|
||
console.log('💰 Token prices retrieved:', prices);
|
||
return prices;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get token prices:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение статистики контракта
|
||
async getContractStats() {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const result = await this.contract.methods.getStats().call();
|
||
|
||
const stats = {
|
||
totalTokens: parseInt(result.totalTokens),
|
||
activeTokens: parseInt(result.activeTokens),
|
||
monthlyTokens: parseInt(result.monthlyTokens),
|
||
yearlyTokens: parseInt(result.yearlyTokens)
|
||
};
|
||
|
||
console.log('📊 Contract stats retrieved:', stats);
|
||
return stats;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get contract stats:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Покупка месячного токена
|
||
async purchaseMonthlyToken(price) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const accounts = await this.web3.eth.getAccounts();
|
||
const userAddress = accounts[0];
|
||
|
||
console.log('🛒 Purchasing monthly token:', { user: userAddress, price: price });
|
||
|
||
const result = await this.contract.methods.purchaseMonthlyToken().send({
|
||
from: userAddress,
|
||
value: price
|
||
});
|
||
|
||
console.log('✅ Monthly token purchased:', result);
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Monthly token purchase failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Покупка годового токена
|
||
async purchaseYearlyToken(price) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const accounts = await this.web3.eth.getAccounts();
|
||
const userAddress = accounts[0];
|
||
|
||
console.log('🛒 Purchasing yearly token:', { user: userAddress, price: price });
|
||
|
||
const result = await this.contract.methods.purchaseYearlyToken().send({
|
||
from: userAddress,
|
||
value: price
|
||
});
|
||
|
||
console.log('✅ Yearly token purchased:', result);
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Yearly token purchase failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Деактивация токена
|
||
async deactivateToken(tokenId) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const accounts = await this.web3.eth.getAccounts();
|
||
const userAddress = accounts[0];
|
||
|
||
console.log('🚫 Deactivating token:', { tokenId, user: userAddress });
|
||
|
||
const result = await this.contract.methods.deactivateToken(tokenId).send({
|
||
from: userAddress
|
||
});
|
||
|
||
console.log('✅ Token deactivated:', result);
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Token deactivation failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Продление токена
|
||
async renewToken(tokenId, price) {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const accounts = await this.web3.eth.getAccounts();
|
||
const userAddress = accounts[0];
|
||
|
||
console.log('🔄 Renewing token:', { tokenId, user: userAddress, price: price });
|
||
|
||
const result = await this.contract.methods.renewToken(tokenId).send({
|
||
from: userAddress,
|
||
value: price
|
||
});
|
||
|
||
console.log('✅ Token renewed:', result);
|
||
return result;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Token renewal failed:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение событий о создании токенов
|
||
async getTokenMintedEvents(fromBlock = 0, toBlock = 'latest') {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const events = await this.contract.getPastEvents('TokenMinted', {
|
||
fromBlock: fromBlock,
|
||
toBlock: toBlock
|
||
});
|
||
|
||
console.log('📝 Token minted events retrieved:', events.length);
|
||
return events;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get token minted events:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Получение событий о продлении токенов
|
||
async getTokenRenewedEvents(fromBlock = 0, toBlock = 'latest') {
|
||
try {
|
||
if (!this.isInitialized || !this.contract) {
|
||
throw new Error('Contract not initialized');
|
||
}
|
||
|
||
const events = await this.contract.getPastEvents('TokenRenewed', {
|
||
fromBlock: fromBlock,
|
||
toBlock: toBlock
|
||
});
|
||
|
||
console.log('🔄 Token renewed events retrieved:', events.length);
|
||
return events;
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to get token renewed events:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Проверка поддержки сети
|
||
isNetworkSupported(chainId) {
|
||
return this.CONTRACT_ADDRESSES.hasOwnProperty(chainId);
|
||
}
|
||
|
||
// Получение поддерживаемых сетей
|
||
getSupportedNetworks() {
|
||
return Object.keys(this.CONTRACT_ADDRESSES).map(chainId => ({
|
||
chainId: chainId,
|
||
name: this.getNetworkName(chainId),
|
||
contractAddress: this.CONTRACT_ADDRESSES[chainId]
|
||
}));
|
||
}
|
||
|
||
// Получение названия сети
|
||
getNetworkName(chainId) {
|
||
const networkNames = {
|
||
'0x1': 'Ethereum Mainnet',
|
||
'0x3': 'Ropsten Testnet',
|
||
'0x5': 'Goerli Testnet',
|
||
'0xaa36a7': 'Sepolia Testnet'
|
||
};
|
||
|
||
return networkNames[chainId] || 'Unknown Network';
|
||
}
|
||
|
||
// Переключение на поддерживаемую сеть
|
||
async switchToNetwork(chainId) {
|
||
try {
|
||
if (!this.isNetworkSupported(chainId)) {
|
||
throw new Error(`Network ${chainId} is not supported`);
|
||
}
|
||
|
||
await window.ethereum.request({
|
||
method: 'wallet_switchEthereumChain',
|
||
params: [{ chainId: chainId }]
|
||
});
|
||
|
||
console.log('🔄 Switched to network:', chainId);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to switch network:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Добавление новой сети
|
||
async addNetwork(chainId, networkName, rpcUrl, blockExplorerUrl) {
|
||
try {
|
||
await window.ethereum.request({
|
||
method: 'wallet_addEthereumChain',
|
||
params: [{
|
||
chainId: chainId,
|
||
chainName: networkName,
|
||
rpcUrls: [rpcUrl],
|
||
blockExplorerUrls: [blockExplorerUrl],
|
||
nativeCurrency: {
|
||
name: 'Ether',
|
||
symbol: 'ETH',
|
||
decimals: 18
|
||
}
|
||
}]
|
||
});
|
||
|
||
console.log('➕ Network added:', { chainId, name: networkName });
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to add network:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Показ сообщений пользователю
|
||
showContractNotDeployedMessage(chainId) {
|
||
const message = `Smart contract not deployed on network ${chainId}. Please switch to a supported network or deploy the contract.`;
|
||
console.warn('⚠️', message);
|
||
// Здесь можно добавить UI уведомление
|
||
}
|
||
|
||
// Получение статуса инициализации
|
||
getInitializationStatus() {
|
||
return {
|
||
isInitialized: this.isInitialized,
|
||
web3: !!this.web3,
|
||
contract: !!this.contract,
|
||
contractAddress: this.contractAddress,
|
||
currentChainId: this.web3 ? this.web3.currentProvider.chainId : null
|
||
};
|
||
}
|
||
|
||
// Очистка ресурсов
|
||
destroy() {
|
||
this.contract = null;
|
||
this.web3 = null;
|
||
this.contractAddress = null;
|
||
this.isInitialized = false;
|
||
|
||
console.log('🗑️ Web3ContractManager destroyed');
|
||
}
|
||
}
|
||
|
||
export { Web3ContractManager };
|