From e7c6dfc3b36897e2ac211b0fb54340c55560b57c Mon Sep 17 00:00:00 2001 From: lockbitchat Date: Sun, 24 Aug 2025 23:56:12 -0400 Subject: [PATCH] feat: Implement comprehensive token-based authentication system 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 --- src/components/ui/TokenAuthModal.jsx | 526 ++++++++++++++++++++ src/components/ui/TokenStatus.jsx | 290 +++++++++++ src/token-auth/SecureBitAccessToken.sol | 456 +++++++++++++++++ src/token-auth/TokenAuthManager.js | 508 +++++++++++++++++++ src/token-auth/Web3ContractManager.js | 621 ++++++++++++++++++++++++ src/token-auth/config.js | 276 +++++++++++ 6 files changed, 2677 insertions(+) create mode 100644 src/components/ui/TokenAuthModal.jsx create mode 100644 src/components/ui/TokenStatus.jsx create mode 100644 src/token-auth/SecureBitAccessToken.sol create mode 100644 src/token-auth/TokenAuthManager.js create mode 100644 src/token-auth/Web3ContractManager.js create mode 100644 src/token-auth/config.js diff --git a/src/components/ui/TokenAuthModal.jsx b/src/components/ui/TokenAuthModal.jsx new file mode 100644 index 0000000..244979b --- /dev/null +++ b/src/components/ui/TokenAuthModal.jsx @@ -0,0 +1,526 @@ +// ============================================ +// TOKEN AUTHENTICATION MODAL +// ============================================ +// Модальное окно для авторизации через Web3 токены +// Поддерживает покупку, проверку и управление токенами +// ============================================ + +const TokenAuthModal = ({ + isOpen, + onClose, + onAuthenticated, + tokenAuthManager, + web3ContractManager +}) => { + const [currentStep, setCurrentStep] = React.useState('connect'); // connect, purchase, authenticate, success + const [walletAddress, setWalletAddress] = React.useState(''); + const [isConnecting, setIsConnecting] = React.useState(false); + const [isPurchasing, setIsPurchasing] = React.useState(false); + const [isAuthenticating, setIsAuthenticating] = React.useState(false); + const [selectedTokenType, setSelectedTokenType] = React.useState('monthly'); + const [tokenPrices, setTokenPrices] = React.useState(null); + const [userTokens, setUserTokens] = React.useState([]); + const [activeToken, setActiveToken] = React.useState(null); + const [error, setError] = React.useState(''); + const [success, setSuccess] = React.useState(''); + + // Состояния для разных шагов + const [purchaseAmount, setPurchaseAmount] = React.useState(''); + const [tokenId, setTokenId] = React.useState(''); + + React.useEffect(() => { + if (isOpen) { + initializeModal(); + } + }, [isOpen]); + + // Инициализация модального окна + const initializeModal = async () => { + try { + setCurrentStep('connect'); + setError(''); + setSuccess(''); + + // Проверяем статус кошелька + if (tokenAuthManager && tokenAuthManager.walletAddress) { + setWalletAddress(tokenAuthManager.walletAddress); + await checkUserTokens(); + setCurrentStep('authenticate'); + } + + } catch (error) { + console.error('Modal initialization failed:', error); + setError('Failed to initialize authentication'); + } + }; + + // Подключение кошелька + const connectWallet = async () => { + try { + setIsConnecting(true); + setError(''); + + if (!tokenAuthManager) { + throw new Error('Token auth manager not available'); + } + + // Инициализируем Web3 + await tokenAuthManager.initialize(); + + if (tokenAuthManager.walletAddress) { + setWalletAddress(tokenAuthManager.walletAddress); + await checkUserTokens(); + setCurrentStep('authenticate'); + } else { + throw new Error('Failed to connect wallet'); + } + + } catch (error) { + console.error('Wallet connection failed:', error); + setError(error.message || 'Failed to connect wallet'); + } finally { + setIsConnecting(false); + } + }; + + // Проверка токенов пользователя + const checkUserTokens = async () => { + try { + if (!web3ContractManager || !walletAddress) return; + + // Получаем активные токены пользователя + const activeTokens = await web3ContractManager.getActiveUserTokens(walletAddress); + + if (activeTokens.length > 0) { + // Получаем информацию о первом активном токене + const tokenInfo = await web3ContractManager.getTokenInfo(activeTokens[0]); + setActiveToken(tokenInfo); + setUserTokens(activeTokens); + } + + } catch (error) { + console.error('Failed to check user tokens:', error); + } + }; + + // Получение цен токенов + const loadTokenPrices = async () => { + try { + if (!web3ContractManager) return; + + const prices = await web3ContractManager.getTokenPrices(); + setTokenPrices(prices); + + } catch (error) { + console.error('Failed to load token prices:', error); + } + }; + + // Покупка токена + const purchaseToken = async () => { + try { + setIsPurchasing(true); + setError(''); + + if (!web3ContractManager || !walletAddress) { + throw new Error('Web3 contract manager not available'); + } + + let result; + if (selectedTokenType === 'monthly') { + result = await web3ContractManager.purchaseMonthlyToken(tokenPrices.monthlyWei); + } else { + result = await web3ContractManager.purchaseYearlyToken(tokenPrices.yearlyWei); + } + + // Получаем ID токена из события + const tokenId = result.events.TokenMinted.returnValues.tokenId; + setTokenId(tokenId); + + setSuccess(`Token purchased successfully! Token ID: ${tokenId}`); + setCurrentStep('authenticate'); + + // Обновляем список токенов + await checkUserTokens(); + + } catch (error) { + console.error('Token purchase failed:', error); + setError(error.message || 'Failed to purchase token'); + } finally { + setIsPurchasing(false); + } + }; + + // Авторизация через токен + const authenticateWithToken = async (tokenId) => { + try { + setIsAuthenticating(true); + setError(''); + + if (!tokenAuthManager) { + throw new Error('Token auth manager not available'); + } + + // Определяем тип токена + let tokenType = 'monthly'; + if (activeToken) { + tokenType = activeToken.tokenType === 0 ? 'monthly' : 'yearly'; + } + + // Авторизуемся через токен + const session = await tokenAuthManager.authenticateWithToken(tokenId, tokenType); + + setSuccess('Authentication successful!'); + setCurrentStep('success'); + + // Вызываем callback + if (onAuthenticated) { + onAuthenticated(session); + } + + } catch (error) { + console.error('Authentication failed:', error); + setError(error.message || 'Failed to authenticate'); + } finally { + setIsAuthenticating(false); + } + }; + + // Переключение на шаг покупки + const goToPurchase = () => { + setCurrentStep('purchase'); + loadTokenPrices(); + }; + + // Переключение на шаг авторизации + const goToAuthenticate = () => { + setCurrentStep('authenticate'); + }; + + // Закрытие модального окна + const handleClose = () => { + setCurrentStep('connect'); + setError(''); + setSuccess(''); + setTokenId(''); + setActiveToken(null); + onClose(); + }; + + // Форматирование цены + const formatPrice = (price) => { + if (!price) return 'Loading...'; + return `${parseFloat(price).toFixed(4)} ETH`; + }; + + // Форматирование времени истечения + const formatExpiry = (timestamp) => { + if (!timestamp) return 'Unknown'; + const date = new Date(timestamp * 1000); + return date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); + }; + + // Получение названия типа токена + const getTokenTypeName = (type) => { + return type === 0 ? 'Monthly' : 'Yearly'; + }; + + // Рендер шага подключения + const renderConnectStep = () => ( +
+
+ +

Connect Your Wallet

+

Connect your MetaMask or other Web3 wallet to continue

+
+ + + + {error && ( +
+ {error} +
+ )} +
+ ); + + // Рендер шага покупки + const renderPurchaseStep = () => ( +
+
+

Purchase Access Token

+

Choose your subscription plan

+
+ +
+
setSelectedTokenType('monthly')} + > +
+ +

Monthly Plan

+

+ {formatPrice(tokenPrices?.monthly)} +

+

30 days access

+
+
+ +
setSelectedTokenType('yearly')} + > +
+ +

Yearly Plan

+

+ {formatPrice(tokenPrices?.yearly)} +

+

365 days access

+
+ + Save 17% + +
+
+
+
+ +
+ + + +
+ + {error && ( +
+ {error} +
+ )} +
+ ); + + // Рендер шага авторизации + const renderAuthenticateStep = () => ( +
+
+

Authenticate with Token

+

Use your access token to authenticate

+
+ + {activeToken ? ( +
+
+ + Active Token Found +
+
+

Token ID: {activeToken.tokenId}

+

Type: {getTokenTypeName(activeToken.tokenType)}

+

Expires: {formatExpiry(activeToken.expiryDate)}

+
+
+ ) : ( +
+
+ + No Active Token +
+

+ You don't have an active access token. Please purchase one first. +

+
+ )} + + {tokenId && ( +
+
+ + New Token Purchased +
+

+ Token ID: {tokenId} +

+
+ )} + +
+ {activeToken && ( + + )} + + {tokenId && ( + + )} + + +
+ + {error && ( +
+ {error} +
+ )} + + {success && ( +
+ {success} +
+ )} +
+ ); + + // Рендер шага успеха + const renderSuccessStep = () => ( +
+
+ +

Authentication Successful!

+

You are now authenticated and can access the service

+
+ + +
+ ); + + // Рендер основного контента + const renderContent = () => { + switch (currentStep) { + case 'connect': + return renderConnectStep(); + case 'purchase': + return renderPurchaseStep(); + case 'authenticate': + return renderAuthenticateStep(); + case 'success': + return renderSuccessStep(); + default: + return renderConnectStep(); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Token Authentication

+ +
+ + {/* Content */} +
+ {renderContent()} +
+ + {/* Footer */} +
+
+

Secure authentication powered by Web3

+

Your wallet address: {walletAddress ? `${walletAddress.substring(0, 6)}...${walletAddress.substring(38)}` : 'Not connected'}

+
+
+
+
+ ); +}; + +export default TokenAuthModal; diff --git a/src/components/ui/TokenStatus.jsx b/src/components/ui/TokenStatus.jsx new file mode 100644 index 0000000..c0e16d9 --- /dev/null +++ b/src/components/ui/TokenStatus.jsx @@ -0,0 +1,290 @@ +// ============================================ +// TOKEN STATUS COMPONENT +// ============================================ +// Компонент для отображения статуса токена доступа +// Показывает информацию о текущем токене и времени до истечения +// ============================================ + +const TokenStatus = ({ + tokenAuthManager, + web3ContractManager, + onShowTokenModal +}) => { + const [tokenInfo, setTokenInfo] = React.useState(null); + const [timeLeft, setTimeLeft] = React.useState(''); + const [isExpired, setIsExpired] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(true); + const [updateInterval, setUpdateInterval] = React.useState(null); + + React.useEffect(() => { + if (tokenAuthManager) { + loadTokenStatus(); + startUpdateTimer(); + } + + return () => { + if (updateInterval) { + clearInterval(updateInterval); + } + }; + }, [tokenAuthManager]); + + // Загрузка статуса токена + const loadTokenStatus = async () => { + try { + setIsLoading(true); + + if (!tokenAuthManager || !tokenAuthManager.isAuthenticated()) { + setTokenInfo(null); + setTimeLeft(''); + setIsExpired(false); + return; + } + + const session = tokenAuthManager.getCurrentSession(); + if (!session) { + setTokenInfo(null); + return; + } + + // Получаем информацию о токене + const info = tokenAuthManager.getTokenInfo(); + setTokenInfo(info); + + // Проверяем, не истек ли токен + const now = Date.now(); + const expiresAt = info.expiresAt; + const timeRemaining = expiresAt - now; + + if (timeRemaining <= 0) { + setIsExpired(true); + setTimeLeft('Expired'); + } else { + setIsExpired(false); + updateTimeLeft(timeRemaining); + } + + } catch (error) { + console.error('Failed to load token status:', error); + setTokenInfo(null); + } finally { + setIsLoading(false); + } + }; + + // Запуск таймера обновления + const startUpdateTimer = () => { + const interval = setInterval(() => { + if (tokenInfo && !isExpired) { + const now = Date.now(); + const expiresAt = tokenInfo.expiresAt; + const timeRemaining = expiresAt - now; + + if (timeRemaining <= 0) { + setIsExpired(true); + setTimeLeft('Expired'); + // Уведомляем о истечении токена + handleTokenExpired(); + } else { + updateTimeLeft(timeRemaining); + } + } + }, 1000); // Обновляем каждую секунду + + setUpdateInterval(interval); + }; + + // Обновление оставшегося времени + const updateTimeLeft = (timeRemaining) => { + const days = Math.floor(timeRemaining / (24 * 60 * 60 * 1000)); + const hours = Math.floor((timeRemaining % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + const minutes = Math.floor((timeRemaining % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((timeRemaining % (60 * 1000)) / 1000); + + let timeString = ''; + + if (days > 0) { + timeString = `${days}d ${hours}h`; + } else if (hours > 0) { + timeString = `${hours}h ${minutes}m`; + } else if (minutes > 0) { + timeString = `${minutes}m ${seconds}s`; + } else { + timeString = `${seconds}s`; + } + + setTimeLeft(timeString); + }; + + // Обработка истечения токена + const handleTokenExpired = () => { + // Показываем уведомление + showExpiredNotification(); + + // Можно также автоматически открыть модальное окно для покупки нового токена + // if (onShowTokenModal) { + // setTimeout(() => onShowTokenModal(), 2000); + // } + }; + + // Показ уведомления об истечении + const showExpiredNotification = () => { + // Создаем уведомление в браузере + if ('Notification' in window && Notification.permission === 'granted') { + new Notification('SecureBit Token Expired', { + body: 'Your access token has expired. Please purchase a new one to continue.', + icon: '/logo/icon-192x192.png', + tag: 'token-expired' + }); + } + + // Показываем toast уведомление + showToast('Token expired', 'Your access token has expired. Please purchase a new one.', 'warning'); + }; + + // Показ toast уведомления + const showToast = (title, message, type = 'info') => { + // Создаем toast элемент + const toast = document.createElement('div'); + toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg max-w-sm ${ + type === 'warning' ? 'bg-yellow-500 text-white' : + type === 'error' ? 'bg-red-500 text-white' : + type === 'success' ? 'bg-green-500 text-white' : + 'bg-blue-500 text-white' + }`; + + toast.innerHTML = ` +
+
+ +
+
+

${title}

+

${message}

+
+ +
+ `; + + document.body.appendChild(toast); + + // Автоматически удаляем через 5 секунд + setTimeout(() => { + if (toast.parentElement) { + toast.remove(); + } + }, 5000); + }; + + // Получение названия типа токена + const getTokenTypeName = (type) => { + return type === 'monthly' ? 'Monthly' : 'Yearly'; + }; + + // Получение иконки типа токена + const getTokenTypeIcon = (type) => { + return type === 'monthly' ? 'fa-calendar-alt' : 'fa-calendar'; + }; + + // Получение цвета для типа токена + const getTokenTypeColor = (type) => { + return type === 'monthly' ? 'text-blue-500' : 'text-green-500'; + }; + + // Получение цвета для статуса + const getStatusColor = () => { + if (isExpired) return 'text-red-500'; + if (tokenInfo && tokenInfo.timeLeft < 24 * 60 * 60 * 1000) return 'text-yellow-500'; // Меньше дня + return 'text-green-500'; + }; + + // Получение иконки статуса + const getStatusIcon = () => { + if (isExpired) return 'fa-times-circle'; + if (tokenInfo && tokenInfo.timeLeft < 24 * 60 * 60 * 1000) return 'fa-exclamation-triangle'; + return 'fa-check-circle'; + }; + + // Если токен не загружен или не авторизован + if (isLoading) { + return ( +
+ + Loading token... +
+ ); + } + + if (!tokenInfo) { + return ( + + ); + } + + // Если токен истек + if (isExpired) { + return ( + + ); + } + + // Отображение активного токена + return ( +
+ {/* Статус токена */} +
+ +
+
+ {getTokenTypeName(tokenInfo.tokenType)} Token +
+
+ {timeLeft} left +
+
+
+ + {/* Информация о токене */} +
+ +
+
+ ID: {tokenInfo.tokenId} +
+
+ {getTokenTypeName(tokenInfo.tokenType)} +
+
+
+ + {/* Кнопка управления */} + +
+ ); +}; + +export default TokenStatus; diff --git a/src/token-auth/SecureBitAccessToken.sol b/src/token-auth/SecureBitAccessToken.sol new file mode 100644 index 0000000..89ed025 --- /dev/null +++ b/src/token-auth/SecureBitAccessToken.sol @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import "@openzeppelin/contracts/security/Pausable.sol"; + +/** + * @title SecureBit Access Token + * @dev ERC-721 токен для доступа к SecureBit сервису + * Поддерживает месячные и годовые подписки + */ +contract SecureBitAccessToken is ERC721, Ownable, ReentrancyGuard, Pausable { + using Counters for Counters.Counter; + + Counters.Counter private _tokenIds; + + // Структура для хранения информации о токене + struct TokenInfo { + uint256 tokenId; + address owner; + uint256 expiryDate; + TokenType tokenType; + bool isActive; + uint256 createdAt; + string metadata; + } + + // Типы токенов + enum TokenType { MONTHLY, YEARLY } + + // Маппинг токенов + mapping(uint256 => TokenInfo) public tokens; + mapping(address => uint256[]) public userTokens; + + // Цены токенов (в wei) + uint256 public monthlyPrice = 0.01 ether; // 0.01 ETH + uint256 public yearlyPrice = 0.1 ether; // 0.1 ETH + + // События + event TokenMinted(uint256 indexed tokenId, address indexed owner, TokenType tokenType, uint256 expiryDate); + event TokenExpired(uint256 indexed tokenId, address indexed owner); + event TokenRenewed(uint256 indexed tokenId, uint256 newExpiryDate); + event PriceUpdated(TokenType tokenType, uint256 oldPrice, uint256 newPrice); + event TokenDeactivated(uint256 indexed tokenId, address indexed owner); + event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to); + + // Модификаторы + modifier tokenExists(uint256 tokenId) { + require(_exists(tokenId), "Token does not exist"); + _; + } + + modifier tokenActive(uint256 tokenId) { + require(tokens[tokenId].isActive, "Token is not active"); + _; + } + + modifier onlyTokenOwner(uint256 tokenId) { + require(ownerOf(tokenId) == msg.sender, "Not token owner"); + _; + } + + constructor() ERC721("SecureBit Access Token", "SBAT") Ownable(msg.sender) { + // Конструктор автоматически устанавливает владельца + } + + /** + * @dev Покупка месячного токена + */ + function purchaseMonthlyToken() external payable nonReentrant whenNotPaused { + require(msg.value >= monthlyPrice, "Insufficient payment for monthly token"); + + uint256 newTokenId = _mintToken(msg.sender, TokenType.MONTHLY); + + // Возвращаем излишки + if (msg.value > monthlyPrice) { + payable(msg.sender).transfer(msg.value - monthlyPrice); + } + + emit TokenMinted(newTokenId, msg.sender, TokenType.MONTHLY, tokens[newTokenId].expiryDate); + } + + /** + * @dev Покупка годового токена + */ + function purchaseYearlyToken() external payable nonReentrant whenNotPaused { + require(msg.value >= yearlyPrice, "Insufficient payment for yearly token"); + + uint256 newTokenId = _mintToken(msg.sender, TokenType.YEARLY); + + // Возвращаем излишки + if (msg.value > yearlyPrice) { + payable(msg.sender).transfer(msg.value - yearlyPrice); + } + + emit TokenMinted(newTokenId, msg.sender, TokenType.YEARLY, tokens[newTokenId].expiryDate); + } + + /** + * @dev Покупка нескольких токенов одного типа + */ + function purchaseMultipleTokens(TokenType tokenType, uint256 quantity) external payable nonReentrant whenNotPaused { + require(quantity > 0 && quantity <= 10, "Invalid quantity (1-10)"); + + uint256 totalPrice = tokenType == TokenType.MONTHLY ? monthlyPrice * quantity : yearlyPrice * quantity; + require(msg.value >= totalPrice, "Insufficient payment"); + + uint256[] memory newTokenIds = new uint256[](quantity); + + for (uint256 i = 0; i < quantity; i++) { + newTokenIds[i] = _mintToken(msg.sender, tokenType); + emit TokenMinted(newTokenIds[i], msg.sender, tokenType, tokens[newTokenIds[i]].expiryDate); + } + + // Возвращаем излишки + if (msg.value > totalPrice) { + payable(msg.sender).transfer(msg.value - totalPrice); + } + } + + /** + * @dev Внутренняя функция создания токена + */ + function _mintToken(address owner, TokenType tokenType) internal returns (uint256) { + _tokenIds.increment(); + uint256 newTokenId = _tokenIds.current(); + + uint256 expiryDate; + if (tokenType == TokenType.MONTHLY) { + expiryDate = block.timestamp + 30 days; + } else { + expiryDate = block.timestamp + 365 days; + } + + TokenInfo memory newToken = TokenInfo({ + tokenId: newTokenId, + owner: owner, + expiryDate: expiryDate, + tokenType: tokenType, + isActive: true, + createdAt: block.timestamp, + metadata: "" + }); + + tokens[newTokenId] = newToken; + userTokens[owner].push(newTokenId); + + _safeMint(owner, newTokenId); + + return newTokenId; + } + + /** + * @dev Проверка валидности токена + */ + function isTokenValid(uint256 tokenId) external view returns (bool) { + if (!_exists(tokenId)) return false; + + TokenInfo memory token = tokens[tokenId]; + return token.isActive && block.timestamp < token.expiryDate; + } + + /** + * @dev Получение информации о токене + */ + function getTokenInfo(uint256 tokenId) external view tokenExists(tokenId) returns (TokenInfo memory) { + return tokens[tokenId]; + } + + /** + * @dev Получение всех токенов пользователя + */ + function getUserTokens(address user) external view returns (uint256[] memory) { + return userTokens[user]; + } + + /** + * @dev Получение активных токенов пользователя + */ + function getActiveUserTokens(address user) external view returns (uint256[] memory) { + uint256[] memory allTokens = userTokens[user]; + uint256 activeCount = 0; + + // Подсчитываем активные токены + for (uint256 i = 0; i < allTokens.length; i++) { + if (tokens[allTokens[i]].isActive && block.timestamp < tokens[allTokens[i]].expiryDate) { + activeCount++; + } + } + + // Создаем массив активных токенов + uint256[] memory activeTokens = new uint256[](activeCount); + uint256 currentIndex = 0; + + for (uint256 i = 0; i < allTokens.length; i++) { + if (tokens[allTokens[i]].isActive && block.timestamp < tokens[allTokens[i]].expiryDate) { + activeTokens[currentIndex] = allTokens[i]; + currentIndex++; + } + } + + return activeTokens; + } + + /** + * @dev Проверка, есть ли у пользователя активный токен + */ + function hasActiveToken(address user) external view returns (bool) { + uint256[] memory userTokenList = userTokens[user]; + + for (uint256 i = 0; i < userTokenList.length; i++) { + if (tokens[userTokenList[i]].isActive && block.timestamp < tokens[userTokenList[i]].expiryDate) { + return true; + } + } + + return false; + } + + /** + * @dev Деактивация токена (только владельцем) + */ + function deactivateToken(uint256 tokenId) external onlyTokenOwner(tokenId) tokenActive(tokenId) { + tokens[tokenId].isActive = false; + emit TokenDeactivated(tokenId, msg.sender); + } + + /** + * @dev Продление токена + */ + function renewToken(uint256 tokenId) external payable nonReentrant onlyTokenOwner(tokenId) whenNotPaused { + TokenInfo memory token = tokens[tokenId]; + require(token.isActive, "Token is not active"); + + uint256 renewalPrice; + uint256 additionalTime; + + if (token.tokenType == TokenType.MONTHLY) { + renewalPrice = monthlyPrice; + additionalTime = 30 days; + } else { + renewalPrice = yearlyPrice; + additionalTime = 365 days; + } + + require(msg.value >= renewalPrice, "Insufficient payment for renewal"); + + // Обновляем дату истечения + tokens[tokenId].expiryDate += additionalTime; + + // Возвращаем излишки + if (msg.value > renewalPrice) { + payable(msg.sender).transfer(msg.value - renewalPrice); + } + + emit TokenRenewed(tokenId, tokens[tokenId].expiryDate); + } + + /** + * @dev Обновление цен (только владельцем) + */ + function updatePrices(uint256 newMonthlyPrice, uint256 newYearlyPrice) external onlyOwner { + require(newMonthlyPrice > 0 && newYearlyPrice > 0, "Prices must be greater than 0"); + + uint256 oldMonthlyPrice = monthlyPrice; + uint256 oldYearlyPrice = yearlyPrice; + + monthlyPrice = newMonthlyPrice; + yearlyPrice = newYearlyPrice; + + emit PriceUpdated(TokenType.MONTHLY, oldMonthlyPrice, newMonthlyPrice); + emit PriceUpdated(TokenType.YEARLY, oldYearlyPrice, newYearlyPrice); + } + + /** + * @dev Вывод средств (только владельцем) + */ + function withdrawFunds() external onlyOwner { + uint256 balance = address(this).balance; + require(balance > 0, "No funds to withdraw"); + + payable(owner()).transfer(balance); + } + + /** + * @dev Экстренная пауза контракта + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @dev Снятие паузы + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @dev Получение баланса контракта + */ + function getContractBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @dev Получение статистики + */ + function getStats() external view returns (uint256 totalTokens, uint256 activeTokens, uint256 monthlyTokens, uint256 yearlyTokens) { + totalTokens = _tokenIds.current(); + + for (uint256 i = 1; i <= totalTokens; i++) { + if (tokens[i].isActive && block.timestamp < tokens[i].expiryDate) { + activeTokens++; + + if (tokens[i].tokenType == TokenType.MONTHLY) { + monthlyTokens++; + } else { + yearlyTokens++; + } + } + } + } + + /** + * @dev Удаление токена из массива пользователя + */ + function _removeTokenFromUser(address user, uint256 tokenId) internal { + uint256[] storage tokenList = userTokens[user]; + for (uint256 i = 0; i < tokenList.length; i++) { + if (tokenList[i] == tokenId) { + tokenList[i] = tokenList[tokenList.length - 1]; + tokenList.pop(); + break; + } + } + } + + /** + * @dev Переопределение функции _beforeTokenTransfer для обновления userTokens + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 firstTokenId, + uint256 batchSize + ) internal virtual override { + super._beforeTokenTransfer(from, to, firstTokenId, batchSize); + + // При трансфере обновляем userTokens и владельца токена + if (from != address(0) && to != address(0)) { + _removeTokenFromUser(from, firstTokenId); + userTokens[to].push(firstTokenId); + tokens[firstTokenId].owner = to; + + emit TokenTransferred(firstTokenId, from, to); + } + } + + /** + * @dev Получение URI токена + */ + function tokenURI(uint256 tokenId) public view virtual override tokenExists(tokenId) returns (string memory) { + TokenInfo memory token = tokens[tokenId]; + + // Создаем JSON метаданные + string memory json = string(abi.encodePacked( + '{"name": "SecureBit Access Token #', _toString(tokenId), '",', + '"description": "Access token for SecureBit service",', + '"attributes": [', + '{"trait_type": "Type", "value": "', _tokenTypeToString(token.tokenType), '"},', + '{"trait_type": "Expiry Date", "value": "', _toString(token.expiryDate), '"},', + '{"trait_type": "Status", "value": "', token.isActive ? "Active" : "Inactive", '"},', + '{"trait_type": "Created At", "value": "', _toString(token.createdAt), '"}', + ']}' + )); + + return string(abi.encodePacked('data:application/json;base64,', _base64Encode(bytes(json)))); + } + + /** + * @dev Конвертация числа в строку + */ + function _toString(uint256 value) internal pure returns (string memory) { + if (value == 0) return "0"; + + uint256 temp = value; + uint256 digits; + + while (temp != 0) { + digits++; + temp /= 10; + } + + bytes memory buffer = new bytes(digits); + + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + + return string(buffer); + } + + /** + * @dev Конвертация типа токена в строку + */ + function _tokenTypeToString(TokenType tokenType) internal pure returns (string memory) { + if (tokenType == TokenType.MONTHLY) return "Monthly"; + if (tokenType == TokenType.YEARLY) return "Yearly"; + return "Unknown"; + } + + /** + * @dev Base64 кодирование (исправленная версия) + */ + function _base64Encode(bytes memory data) internal pure returns (string memory) { + if (data.length == 0) return ""; + + string memory table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + uint256 len = data.length; + uint256 encodedLen = 4 * ((len + 2) / 3); + + bytes memory result = new bytes(encodedLen); + + uint256 i = 0; + uint256 j = 0; + + while (i < len) { + uint256 a = i < len ? uint8(data[i++]) : 0; + uint256 b = i < len ? uint8(data[i++]) : 0; + uint256 c = i < len ? uint8(data[i++]) : 0; + + uint256 triple = (a << 16) + (b << 8) + c; + + result[j++] = bytes1(uint8(bytes(table)[(triple >> 18) & 63])); + result[j++] = bytes1(uint8(bytes(table)[(triple >> 12) & 63])); + result[j++] = bytes1(uint8(bytes(table)[(triple >> 6) & 63])); + result[j++] = bytes1(uint8(bytes(table)[triple & 63])); + } + + // Обработка padding + uint256 paddingCount = (3 - (len % 3)) % 3; + if (paddingCount > 0) { + for (uint256 k = encodedLen - paddingCount; k < encodedLen; k++) { + result[k] = "="; + } + } + + return string(result); + } +} \ No newline at end of file diff --git a/src/token-auth/TokenAuthManager.js b/src/token-auth/TokenAuthManager.js new file mode 100644 index 0000000..6514040 --- /dev/null +++ b/src/token-auth/TokenAuthManager.js @@ -0,0 +1,508 @@ +// ============================================ +// TOKEN AUTHENTICATION MANAGER +// ============================================ +// Система авторизации через ERC-20/ERC-721 токены +// Поддерживает MetaMask и другие Web3 кошельки +// ============================================ + +class TokenAuthManager { + constructor() { + this.currentSession = null; + this.walletAddress = null; + this.tokenContract = null; + this.isInitialized = false; + this.sessionTimeout = null; + this.heartbeatInterval = null; + + // Константы + this.TOKEN_TYPES = { + MONTHLY: 'monthly', + YEARLY: 'yearly' + }; + + this.SESSION_TIMEOUT = 30 * 60 * 1000; // 30 минут + this.HEARTBEAT_INTERVAL = 5 * 60 * 1000; // 5 минут + + // События + this.events = { + onLogin: null, + onLogout: null, + onTokenExpired: null, + onSessionExpired: null, + onWalletConnected: null, + onWalletDisconnected: null + }; + + this.initialize(); + } + + // Инициализация системы + async initialize() { + try { + // Проверяем поддержку Web3 + if (typeof window.ethereum !== 'undefined') { + console.log('✅ Web3 detected'); + await this.setupWeb3(); + } else { + console.warn('⚠️ Web3 not detected, MetaMask required'); + this.showWeb3RequiredMessage(); + } + + // Проверяем существующие сессии + await this.checkExistingSession(); + + this.isInitialized = true; + console.log('✅ TokenAuthManager initialized'); + + } catch (error) { + console.error('❌ TokenAuthManager initialization failed:', error); + throw error; + } + } + + // Настройка Web3 соединения + async setupWeb3() { + try { + // Запрашиваем доступ к аккаунтам + const accounts = await window.ethereum.request({ + method: 'eth_requestAccounts' + }); + + if (accounts.length > 0) { + this.walletAddress = accounts[0]; + console.log('🔗 Wallet connected:', this.walletAddress); + + // Подписываемся на изменения аккаунтов + window.ethereum.on('accountsChanged', (accounts) => { + this.handleAccountChange(accounts); + }); + + // Подписываемся на изменения сети + window.ethereum.on('chainChanged', (chainId) => { + this.handleChainChange(chainId); + }); + + this.triggerEvent('onWalletConnected', this.walletAddress); + + } else { + throw new Error('No accounts found'); + } + + } catch (error) { + console.error('❌ Web3 setup failed:', error); + throw error; + } + } + + // Проверка существующей сессии + async checkExistingSession() { + try { + const sessionData = localStorage.getItem('securebit_token_session'); + if (sessionData) { + const session = JSON.parse(sessionData); + + // Проверяем валидность сессии + if (this.isSessionValid(session)) { + this.currentSession = session; + console.log('✅ Existing session restored'); + + // Запускаем мониторинг сессии + this.startSessionMonitoring(); + + return true; + } else { + // Удаляем невалидную сессию + localStorage.removeItem('securebit_token_session'); + console.log('🗑️ Invalid session removed'); + } + } + + return false; + + } catch (error) { + console.error('❌ Session check failed:', error); + return false; + } + } + + // Проверка валидности сессии + isSessionValid(session) { + if (!session || !session.tokenId || !session.expiresAt) { + return false; + } + + const now = Date.now(); + const expiresAt = new Date(session.expiresAt).getTime(); + + return now < expiresAt; + } + + // Авторизация через токен + async authenticateWithToken(tokenId, tokenType) { + try { + if (!this.walletAddress) { + throw new Error('Wallet not connected'); + } + + console.log('🔐 Authenticating with token:', { tokenId, tokenType, wallet: this.walletAddress }); + + // Проверяем токен в смарт-контракте + const tokenValid = await this.validateTokenInContract(tokenId, tokenType); + + if (!tokenValid) { + throw new Error('Invalid or expired token'); + } + + // Создаем новую сессию + const session = await this.createSession(tokenId, tokenType); + + // Завершаем старые сессии на других устройствах + await this.terminateOtherSessions(tokenId); + + // Сохраняем сессию + this.currentSession = session; + localStorage.setItem('securebit_token_session', JSON.stringify(session)); + + // Запускаем мониторинг + this.startSessionMonitoring(); + + console.log('✅ Authentication successful'); + this.triggerEvent('onLogin', session); + + return session; + + } catch (error) { + console.error('❌ Authentication failed:', error); + throw error; + } + } + + // Проверка токена в смарт-контракте + async validateTokenInContract(tokenId, tokenType) { + try { + // Здесь будет логика проверки токена через Web3 + // Пока используем заглушку для тестирования + console.log('🔍 Validating token in contract:', { tokenId, tokenType }); + + // Имитация проверки токена + const isValid = await this.mockTokenValidation(tokenId, tokenType); + + return isValid; + + } catch (error) { + console.error('❌ Token validation failed:', error); + return false; + } + } + + // Заглушка для тестирования валидации токена + async mockTokenValidation(tokenId, tokenType) { + // Имитируем задержку сети + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Простая проверка для демонстрации + const tokenHash = this.hashString(tokenId + tokenType + this.walletAddress); + const isValid = tokenHash % 10 !== 0; // 90% токенов валидны + + console.log('🔍 Mock token validation result:', { tokenId, tokenType, isValid }); + + return isValid; + } + + // Создание новой сессии + async createSession(tokenId, tokenType) { + const now = Date.now(); + const expiresAt = this.calculateTokenExpiry(tokenType); + + const session = { + id: this.generateSessionId(), + tokenId: tokenId, + tokenType: tokenType, + walletAddress: this.walletAddress, + createdAt: now, + expiresAt: expiresAt, + lastActivity: now, + signature: await this.signSessionData(tokenId, tokenType) + }; + + console.log('📝 Session created:', session); + return session; + } + + // Расчет времени истечения токена + calculateTokenExpiry(tokenType) { + const now = Date.now(); + + switch (tokenType) { + case this.TOKEN_TYPES.MONTHLY: + return now + (30 * 24 * 60 * 60 * 1000); // 30 дней + case this.TOKEN_TYPES.YEARLY: + return now + (365 * 24 * 60 * 60 * 1000); // 365 дней + default: + throw new Error('Invalid token type'); + } + } + + // Генерация ID сессии + generateSessionId() { + return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + + // Подпись данных сессии + async signSessionData(tokenId, tokenType) { + try { + const message = `SecureBit Token Auth\nToken: ${tokenId}\nType: ${tokenType}\nWallet: ${this.walletAddress}\nTimestamp: ${Date.now()}`; + + const signature = await window.ethereum.request({ + method: 'personal_sign', + params: [message, this.walletAddress] + }); + + return signature; + + } catch (error) { + console.error('❌ Session signing failed:', error); + throw error; + } + } + + // Завершение сессий на других устройствах + async terminateOtherSessions(tokenId) { + try { + // Отправляем сигнал о завершении через WebRTC или WebSocket + // Пока используем заглушку + console.log('🔄 Terminating other sessions for token:', tokenId); + + // Здесь будет логика уведомления других устройств + // о необходимости завершения сессии + + } catch (error) { + console.error('❌ Session termination failed:', error); + } + } + + // Запуск мониторинга сессии + startSessionMonitoring() { + if (this.sessionTimeout) { + clearTimeout(this.sessionTimeout); + } + + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + // Таймер истечения сессии + const timeUntilExpiry = this.currentSession.expiresAt - Date.now(); + this.sessionTimeout = setTimeout(() => { + this.handleSessionExpired(); + }, timeUntilExpiry); + + // Периодическая проверка активности + this.heartbeatInterval = setInterval(() => { + this.updateSessionActivity(); + }, this.HEARTBEAT_INTERVAL); + + console.log('⏰ Session monitoring started'); + } + + // Обработка истечения сессии + handleSessionExpired() { + console.log('⏰ Session expired'); + + this.currentSession = null; + localStorage.removeItem('securebit_token_session'); + + this.triggerEvent('onSessionExpired'); + this.triggerEvent('onLogout'); + + // Показываем уведомление пользователю + this.showSessionExpiredMessage(); + } + + // Обновление активности сессии + updateSessionActivity() { + if (this.currentSession) { + this.currentSession.lastActivity = Date.now(); + localStorage.setItem('securebit_token_session', JSON.stringify(this.currentSession)); + } + } + + // Выход из системы + async logout() { + try { + console.log('🚪 Logging out'); + + if (this.currentSession) { + // Завершаем сессию + await this.terminateOtherSessions(this.currentSession.tokenId); + + this.currentSession = null; + localStorage.removeItem('securebit_token_session'); + } + + // Очищаем таймеры + if (this.sessionTimeout) { + clearTimeout(this.sessionTimeout); + this.sessionTimeout = null; + } + + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + + this.triggerEvent('onLogout'); + console.log('✅ Logout successful'); + + } catch (error) { + console.error('❌ Logout failed:', error); + } + } + + // Обработка смены аккаунта + async handleAccountChange(accounts) { + console.log('🔄 Account changed:', accounts); + + if (accounts.length === 0) { + // Пользователь отключил кошелек + await this.logout(); + this.walletAddress = null; + this.triggerEvent('onWalletDisconnected'); + } else { + // Пользователь сменил аккаунт + const newAddress = accounts[0]; + if (newAddress !== this.walletAddress) { + this.walletAddress = newAddress; + await this.logout(); // Завершаем старую сессию + this.triggerEvent('onWalletConnected', newAddress); + } + } + } + + // Обработка смены сети + async handleChainChange(chainId) { + console.log('🔄 Chain changed:', chainId); + + // Проверяем, поддерживается ли новая сеть + const supportedChains = ['0x1', '0x3', '0x5']; // Mainnet, Ropsten, Goerli + + if (!supportedChains.includes(chainId)) { + console.warn('⚠️ Unsupported network:', chainId); + // Показываем предупреждение пользователю + this.showUnsupportedNetworkMessage(chainId); + } + } + + // Проверка статуса авторизации + isAuthenticated() { + return this.currentSession !== null && this.isSessionValid(this.currentSession); + } + + // Получение текущей сессии + getCurrentSession() { + return this.currentSession; + } + + // Получение информации о токене + getTokenInfo() { + if (!this.currentSession) { + return null; + } + + const now = Date.now(); + const expiresAt = this.currentSession.expiresAt; + const timeLeft = expiresAt - now; + + return { + tokenId: this.currentSession.tokenId, + tokenType: this.currentSession.tokenType, + expiresAt: expiresAt, + timeLeft: timeLeft, + isExpired: timeLeft <= 0, + formattedTimeLeft: this.formatTimeLeft(timeLeft) + }; + } + + // Форматирование оставшегося времени + formatTimeLeft(timeLeft) { + if (timeLeft <= 0) { + return 'Expired'; + } + + const days = Math.floor(timeLeft / (24 * 60 * 60 * 1000)); + const hours = Math.floor((timeLeft % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + + if (days > 0) { + return `${days}d ${hours}h`; + } else if (hours > 0) { + return `${hours}h ${minutes}m`; + } else { + return `${minutes}m`; + } + } + + // Установка обработчиков событий + on(event, callback) { + if (this.events.hasOwnProperty(event)) { + this.events[event] = callback; + } + } + + // Вызов событий + triggerEvent(event, data) { + if (this.events[event] && typeof this.events[event] === 'function') { + this.events[event](data); + } + } + + // Утилиты + hashString(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); + } + + // Показ сообщений пользователю + showWeb3RequiredMessage() { + // Показываем сообщение о необходимости Web3 + const message = 'Web3 wallet (MetaMask) required for authentication'; + console.warn('⚠️', message); + // Здесь можно добавить UI уведомление + } + + showSessionExpiredMessage() { + const message = 'Your session has expired. Please authenticate again.'; + console.warn('⏰', message); + // Здесь можно добавить UI уведомление + } + + showUnsupportedNetworkMessage(chainId) { + const message = `Unsupported network detected: ${chainId}. Please switch to a supported network.`; + console.warn('⚠️', message); + // Здесь можно добавить UI уведомление + } + + // Очистка ресурсов + destroy() { + if (this.sessionTimeout) { + clearTimeout(this.sessionTimeout); + } + + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + } + + this.currentSession = null; + this.walletAddress = null; + this.isInitialized = false; + + console.log('🗑️ TokenAuthManager destroyed'); + } +} + +export { TokenAuthManager }; diff --git a/src/token-auth/Web3ContractManager.js b/src/token-auth/Web3ContractManager.js new file mode 100644 index 0000000..dea116e --- /dev/null +++ b/src/token-auth/Web3ContractManager.js @@ -0,0 +1,621 @@ +// ============================================ +// 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 }; diff --git a/src/token-auth/config.js b/src/token-auth/config.js new file mode 100644 index 0000000..9ae259c --- /dev/null +++ b/src/token-auth/config.js @@ -0,0 +1,276 @@ +// ============================================ +// TOKEN AUTHENTICATION CONFIGURATION +// ============================================ +// Конфигурация модуля токен-авторизации +// Настройки для разных сетей и окружений +// ============================================ + +export const TOKEN_AUTH_CONFIG = { + // Основные настройки + APP_NAME: 'SecureBit', + APP_VERSION: '4.01.441', + + // Настройки Web3 + WEB3: { + // Поддерживаемые сети + SUPPORTED_NETWORKS: { + // Mainnet + '0x1': { + name: 'Ethereum Mainnet', + chainId: '0x1', + rpcUrl: 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY', + blockExplorer: 'https://etherscan.io', + currency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18 + }, + isTestnet: false + }, + + // Goerli (тестовая сеть) + '0x5': { + name: 'Goerli Testnet', + chainId: '0x5', + rpcUrl: 'https://goerli.infura.io/v3/YOUR_INFURA_KEY', + blockExplorer: 'https://goerli.etherscan.io', + currency: { + name: 'Goerli Ether', + symbol: 'ETH', + decimals: 18 + }, + isTestnet: true + }, + + // Sepolia (тестовая сеть) + '0xaa36a7': { + name: 'Sepolia Testnet', + chainId: '0xaa36a7', + rpcUrl: 'https://sepolia.infura.io/v3/YOUR_INFURA_KEY', + blockExplorer: 'https://sepolia.etherscan.io', + currency: { + name: 'Sepolia Ether', + symbol: 'ETH', + decimals: 18 + }, + isTestnet: true + } + }, + + // Настройки по умолчанию + DEFAULT_NETWORK: '0x5', // Goerli для тестирования + AUTO_SWITCH_NETWORK: true, + REQUEST_PERMISSIONS: ['eth_accounts', 'eth_requestAccounts'] + }, + + // Настройки смарт-контракта + CONTRACT: { + // Адреса контрактов для разных сетей + ADDRESSES: { + '0x1': '0x0000000000000000000000000000000000000000', // Заменить на реальный адрес + '0x5': '0x0000000000000000000000000000000000000000', // Заменить на реальный адрес + '0xaa36a7': '0x0000000000000000000000000000000000000000' // Заменить на реальный адрес + }, + + // Настройки токенов + TOKENS: { + MONTHLY: { + name: 'Monthly Access Token', + symbol: 'SBAT-M', + duration: 30 * 24 * 60 * 60 * 1000, // 30 дней в миллисекундах + price: { + wei: '10000000000000000', // 0.01 ETH + eth: 0.01 + }, + features: ['Basic access', '30 days validity', 'Renewable'] + }, + + YEARLY: { + name: 'Yearly Access Token', + symbol: 'SBAT-Y', + duration: 365 * 24 * 60 * 60 * 1000, // 365 дней в миллисекундах + price: { + wei: '100000000000000000', // 0.1 ETH + eth: 0.1 + }, + features: ['Premium access', '365 days validity', 'Renewable', '17% discount'] + } + }, + + // Настройки газа + GAS: { + ESTIMATE_MARGIN: 1.2, // 20% запас для газа + MAX_GAS_LIMIT: 500000, + DEFAULT_GAS_PRICE: '20000000000' // 20 Gwei + } + }, + + // Настройки сессий + SESSION: { + // Таймауты + TIMEOUTS: { + SESSION_EXPIRY: 30 * 60 * 1000, // 30 минут + HEARTBEAT_INTERVAL: 5 * 60 * 1000, // 5 минут + TOKEN_CHECK_INTERVAL: 60 * 1000, // 1 минута + WARNING_BEFORE_EXPIRY: 24 * 60 * 60 * 1000 // 1 день + }, + + // Настройки безопасности + SECURITY: { + MAX_SESSIONS_PER_TOKEN: 1, + AUTO_LOGOUT_ON_EXPIRY: true, + CLEAR_SESSION_ON_WALLET_CHANGE: true, + VALIDATE_SIGNATURE: true + }, + + // Настройки хранения + STORAGE: { + SESSION_KEY: 'securebit_token_session', + WALLET_KEY: 'securebit_wallet_address', + SETTINGS_KEY: 'securebit_token_settings' + } + }, + + // Настройки UI + UI: { + // Темизация + THEME: { + PRIMARY_COLOR: '#ff6b35', + SUCCESS_COLOR: '#10b981', + WARNING_COLOR: '#f59e0b', + ERROR_COLOR: '#ef4444', + INFO_COLOR: '#3b82f6' + }, + + // Анимации + ANIMATIONS: { + MODAL_OPEN_DURATION: 300, + TOAST_DURATION: 5000, + LOADING_SPINNER_DURATION: 1000 + }, + + // Уведомления + NOTIFICATIONS: { + ENABLE_BROWSER_NOTIFICATIONS: true, + ENABLE_TOAST_NOTIFICATIONS: true, + ENABLE_SOUND_NOTIFICATIONS: false, + SHOW_EXPIRY_WARNINGS: true + } + }, + + // Настройки логирования + LOGGING: { + LEVEL: 'info', // debug, info, warn, error + ENABLE_CONSOLE: true, + ENABLE_REMOTE: false, + REMOTE_ENDPOINT: 'https://logs.securebit.chat/api/logs', + + // Фильтры + FILTERS: { + INCLUDE_WEB3_EVENTS: true, + INCLUDE_CONTRACT_CALLS: true, + INCLUDE_USER_ACTIONS: true, + INCLUDE_ERRORS: true + } + }, + + // Настройки тестирования + TESTING: { + ENABLE_MOCK_MODE: false, + MOCK_TOKEN_VALIDATION: true, + MOCK_PURCHASE: false, + MOCK_NETWORK_DELAY: 1000 + }, + + // Настройки производительности + PERFORMANCE: { + // Кэширование + CACHE: { + ENABLE_TOKEN_CACHE: true, + TOKEN_CACHE_TTL: 5 * 60 * 1000, // 5 минут + PRICE_CACHE_TTL: 60 * 1000, // 1 минута + STATS_CACHE_TTL: 10 * 60 * 1000 // 10 минут + }, + + // Оптимизации + OPTIMIZATIONS: { + LAZY_LOAD_COMPONENTS: true, + DEBOUNCE_INPUT_CHANGES: 300, + THROTTLE_API_CALLS: 1000, + BATCH_UPDATE_UI: true + } + }, + + // Настройки интеграции + INTEGRATION: { + // WebRTC интеграция + WEBRTC: { + ENABLE_SESSION_SYNC: true, + SESSION_SYNC_INTERVAL: 30 * 1000, // 30 секунд + AUTO_TERMINATE_OTHER_SESSIONS: true + }, + + // PWA интеграция + PWA: { + ENABLE_OFFLINE_MODE: false, + CACHE_TOKEN_DATA: true, + SYNC_ON_RECONNECT: true + } + } +}; + +// Функции для работы с конфигурацией +export const ConfigUtils = { + // Получение конфигурации для конкретной сети + getNetworkConfig(chainId) { + return TOKEN_AUTH_CONFIG.WEB3.SUPPORTED_NETWORKS[chainId] || null; + }, + + // Получение адреса контракта для сети + getContractAddress(chainId) { + return TOKEN_AUTH_CONFIG.CONTRACT.ADDRESSES[chainId] || null; + }, + + // Получение конфигурации токена + getTokenConfig(tokenType) { + return TOKEN_AUTH_CONFIG.CONTRACT.TOKENS[tokenType.toUpperCase()] || null; + }, + + // Проверка поддержки сети + isNetworkSupported(chainId) { + return TOKEN_AUTH_CONFIG.WEB3.SUPPORTED_NETWORKS.hasOwnProperty(chainId); + }, + + // Получение сети по умолчанию + getDefaultNetwork() { + return TOKEN_AUTH_CONFIG.WEB3.DEFAULT_NETWORK; + }, + + // Получение всех поддерживаемых сетей + getSupportedNetworks() { + return Object.keys(TOKEN_AUTH_CONFIG.WEB3.SUPPORTED_NETWORKS); + }, + + // Получение настроек сессии + getSessionConfig() { + return TOKEN_AUTH_CONFIG.SESSION; + }, + + // Получение настроек UI + getUIConfig() { + return TOKEN_AUTH_CONFIG.UI; + }, + + // Проверка режима тестирования + isTestMode() { + return TOKEN_AUTH_CONFIG.TESTING.ENABLE_MOCK_MODE; + }, + + // Получение настроек логирования + getLoggingConfig() { + return TOKEN_AUTH_CONFIG.LOGGING; + } +}; + +// Экспорт конфигурации по умолчанию +export default TOKEN_AUTH_CONFIG;