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
This commit is contained in:
526
src/components/ui/TokenAuthModal.jsx
Normal file
526
src/components/ui/TokenAuthModal.jsx
Normal file
@@ -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 = () => (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-6">
|
||||||
|
<i className="fas fa-wallet text-4xl text-blue-500 mb-4"></i>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Connect Your Wallet</h3>
|
||||||
|
<p className="text-gray-600">Connect your MetaMask or other Web3 wallet to continue</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={connectWallet}
|
||||||
|
disabled={isConnecting}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{isConnecting ? (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-spinner fa-spin mr-2"></i>
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-wallet mr-2"></i>
|
||||||
|
Connect Wallet
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рендер шага покупки
|
||||||
|
const renderPurchaseStep = () => (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Purchase Access Token</h3>
|
||||||
|
<p className="text-gray-600">Choose your subscription plan</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div
|
||||||
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
|
||||||
|
selectedTokenType === 'monthly'
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedTokenType('monthly')}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<i className="fas fa-calendar-alt text-2xl text-blue-500 mb-2"></i>
|
||||||
|
<h4 className="font-semibold">Monthly Plan</h4>
|
||||||
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{formatPrice(tokenPrices?.monthly)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">30 days access</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-colors ${
|
||||||
|
selectedTokenType === 'yearly'
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedTokenType('yearly')}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<i className="fas fa-calendar text-2xl text-green-500 mb-2"></i>
|
||||||
|
<h4 className="font-semibold">Yearly Plan</h4>
|
||||||
|
<p className="text-2xl font-bold text-green-600">
|
||||||
|
{formatPrice(tokenPrices?.yearly)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">365 days access</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full">
|
||||||
|
Save 17%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentStep('connect')}
|
||||||
|
className="text-gray-600 hover:text-gray-800 transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-arrow-left mr-2"></i>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={purchaseToken}
|
||||||
|
disabled={isPurchasing || !tokenPrices}
|
||||||
|
className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{isPurchasing ? (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-spinner fa-spin mr-2"></i>
|
||||||
|
Purchasing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-credit-card mr-2"></i>
|
||||||
|
Purchase Token
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рендер шага авторизации
|
||||||
|
const renderAuthenticateStep = () => (
|
||||||
|
<div>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Authenticate with Token</h3>
|
||||||
|
<p className="text-gray-600">Use your access token to authenticate</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeToken ? (
|
||||||
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<i className="fas fa-check-circle text-green-500 mr-2"></i>
|
||||||
|
<span className="font-semibold text-green-800">Active Token Found</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-green-700">
|
||||||
|
<p><strong>Token ID:</strong> {activeToken.tokenId}</p>
|
||||||
|
<p><strong>Type:</strong> {getTokenTypeName(activeToken.tokenType)}</p>
|
||||||
|
<p><strong>Expires:</strong> {formatExpiry(activeToken.expiryDate)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<i className="fas fa-exclamation-triangle text-yellow-500 mr-2"></i>
|
||||||
|
<span className="font-semibold text-yellow-800">No Active Token</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-yellow-700">
|
||||||
|
You don't have an active access token. Please purchase one first.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tokenId && (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
|
||||||
|
<div className="flex items-center mb-2">
|
||||||
|
<i className="fas fa-info-circle text-blue-500 mr-2"></i>
|
||||||
|
<span className="font-semibold text-blue-800">New Token Purchased</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-blue-700">
|
||||||
|
<strong>Token ID:</strong> {tokenId}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{activeToken && (
|
||||||
|
<button
|
||||||
|
onClick={() => authenticateWithToken(activeToken.tokenId)}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="w-full bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-spinner fa-spin mr-2"></i>
|
||||||
|
Authenticating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-sign-in-alt mr-2"></i>
|
||||||
|
Authenticate with Active Token
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tokenId && (
|
||||||
|
<button
|
||||||
|
onClick={() => authenticateWithToken(tokenId)}
|
||||||
|
disabled={isAuthenticating}
|
||||||
|
className="w-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
{isAuthenticating ? (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-spinner fa-spin mr-2"></i>
|
||||||
|
Authenticating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-sign-in-alt mr-2"></i>
|
||||||
|
Authenticate with New Token
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goToPurchase}
|
||||||
|
className="w-full bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-plus mr-2"></i>
|
||||||
|
Purchase New Token
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mt-4 p-3 bg-red-100 border border-red-300 text-red-700 rounded-lg">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="mt-4 p-3 bg-green-100 border border-green-300 text-green-700 rounded-lg">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рендер шага успеха
|
||||||
|
const renderSuccessStep = () => (
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-6">
|
||||||
|
<i className="fas fa-check-circle text-6xl text-green-500 mb-4"></i>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">Authentication Successful!</h3>
|
||||||
|
<p className="text-gray-600">You are now authenticated and can access the service</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-check mr-2"></i>
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рендер основного контента
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b">
|
||||||
|
<h2 className="text-xl font-semibold">Token Authentication</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-times text-xl"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-6 border-t bg-gray-50">
|
||||||
|
<div className="text-center text-sm text-gray-600">
|
||||||
|
<p>Secure authentication powered by Web3</p>
|
||||||
|
<p className="mt-1">Your wallet address: {walletAddress ? `${walletAddress.substring(0, 6)}...${walletAddress.substring(38)}` : 'Not connected'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenAuthModal;
|
||||||
290
src/components/ui/TokenStatus.jsx
Normal file
290
src/components/ui/TokenStatus.jsx
Normal file
@@ -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 = `
|
||||||
|
<div class="flex items-start">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-${type === 'warning' ? 'exclamation-triangle' :
|
||||||
|
type === 'error' ? 'times-circle' :
|
||||||
|
type === 'success' ? 'check-circle' : 'info-circle'}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<p class="font-medium">${title}</p>
|
||||||
|
<p class="text-sm opacity-90">${message}</p>
|
||||||
|
</div>
|
||||||
|
<button class="ml-4 text-white opacity-70 hover:opacity-100" onclick="this.parentElement.parentElement.remove()">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center space-x-2 px-3 py-2 bg-gray-100 rounded-lg">
|
||||||
|
<i className="fas fa-spinner fa-spin text-gray-400"></i>
|
||||||
|
<span className="text-sm text-gray-500">Loading token...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenInfo) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onShowTokenModal}
|
||||||
|
className="flex items-center space-x-2 px-3 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-key"></i>
|
||||||
|
<span className="text-sm font-medium">Connect Token</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если токен истек
|
||||||
|
if (isExpired) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onShowTokenModal}
|
||||||
|
className="flex items-center space-x-2 px-3 py-2 bg-red-100 hover:bg-red-200 text-red-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<i className="fas fa-exclamation-triangle"></i>
|
||||||
|
<span className="text-sm font-medium">Token Expired</span>
|
||||||
|
<i className="fas fa-arrow-right text-xs"></i>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображение активного токена
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{/* Статус токена */}
|
||||||
|
<div className="flex items-center space-x-2 px-3 py-2 bg-green-100 rounded-lg">
|
||||||
|
<i className={`fas ${getStatusIcon()} ${getStatusColor()}`}></i>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-medium text-gray-800">
|
||||||
|
{getTokenTypeName(tokenInfo.tokenType)} Token
|
||||||
|
</div>
|
||||||
|
<div className={`text-xs ${getStatusColor()}`}>
|
||||||
|
{timeLeft} left
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Информация о токене */}
|
||||||
|
<div className="hidden md:flex items-center space-x-2 px-3 py-2 bg-gray-100 rounded-lg">
|
||||||
|
<i className={`fas ${getTokenTypeIcon(tokenInfo.tokenType)} ${getTokenTypeColor(tokenInfo.tokenType)}`}></i>
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="text-gray-800">
|
||||||
|
ID: {tokenInfo.tokenId}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{getTokenTypeName(tokenInfo.tokenType)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Кнопка управления */}
|
||||||
|
<button
|
||||||
|
onClick={onShowTokenModal}
|
||||||
|
className="flex items-center space-x-2 px-3 py-2 bg-blue-100 hover:bg-blue-200 text-blue-700 rounded-lg transition-colors"
|
||||||
|
title="Manage token"
|
||||||
|
>
|
||||||
|
<i className="fas fa-cog"></i>
|
||||||
|
<span className="hidden sm:inline text-sm font-medium">Manage</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TokenStatus;
|
||||||
456
src/token-auth/SecureBitAccessToken.sol
Normal file
456
src/token-auth/SecureBitAccessToken.sol
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
508
src/token-auth/TokenAuthManager.js
Normal file
508
src/token-auth/TokenAuthManager.js
Normal file
@@ -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 };
|
||||||
621
src/token-auth/Web3ContractManager.js
Normal file
621
src/token-auth/Web3ContractManager.js
Normal file
@@ -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 };
|
||||||
276
src/token-auth/config.js
Normal file
276
src/token-auth/config.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user