Files
securebit-chat/src/token-auth/SecureBitAccessToken.sol
T
lockbitchat e7c6dfc3b3 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
2025-08-24 23:56:12 -04:00

456 lines
16 KiB
Solidity

// 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);
}
}