init auth and main function

This commit is contained in:
Yuriy Yuriev
2026-05-14 16:15:58 +07:00
commit 27c53e897e
47 changed files with 5750 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
from .manager import AuthManager
from .storage import AuthStorage
__all__ = ["AuthManager", "AuthStorage"]
+74
View File
@@ -0,0 +1,74 @@
import asyncio
import hashlib
import secrets
import logging
from datetime import datetime, timedelta
from typing import Dict, Optional
logger = logging.getLogger(__name__)
class AuthManager:
"""Управляет авторизацией: хеширование паролей, сессии, верификация."""
def __init__(self, session_timeout_minutes: int = 120):
self._sessions: Dict[int, datetime] = {}
self._failed_attempts: Dict[int, list] = {}
self._max_attempts = 5
self._lockout_duration = 300 # 5 минут блокировки
self._session_timeout = timedelta(minutes=session_timeout_minutes)
@staticmethod
def _hash_password(password: str, salt: str = None) -> tuple:
if salt is None:
salt = secrets.token_hex(16)
result = hashlib.sha256((salt + password).encode()).hexdigest()
return result, salt
def verify_password(self, password: str, password_hash: str, salt: str) -> bool:
computed, _ = self._hash_password(password, salt)
return secrets.compare_digest(computed, password_hash)
def is_authenticated(self, user_id: int) -> bool:
if user_id not in self._sessions:
return False
if datetime.now() - self._sessions[user_id] > self._session_timeout:
self.logout(user_id)
return False
return True
def login(self, user_id: int):
self._sessions[user_id] = datetime.now()
self._failed_attempts.pop(user_id, None)
logger.info(f"User {user_id} logged in")
def logout(self, user_id: int):
self._sessions.pop(user_id, None)
self._failed_attempts.pop(user_id, None)
logger.info(f"User {user_id} logged out")
def is_locked_out(self, user_id: int) -> bool:
if user_id not in self._failed_attempts:
return False
attempts = self._failed_attempts[user_id]
recent = [t for t in attempts if datetime.now() - t < timedelta(minutes=5)]
self._failed_attempts[user_id] = recent
if len(recent) >= self._max_attempts:
return True
return False
def record_failed_attempt(self, user_id: int):
if user_id not in self._failed_attempts:
self._failed_attempts[user_id] = []
self._failed_attempts[user_id].append(datetime.now())
def get_session_info(self, user_id: int) -> Optional[dict]:
if user_id not in self._sessions:
return None
login_time = self._sessions[user_id]
elapsed = datetime.now() - login_time
return {
"user_id": user_id,
"login_time": login_time.isoformat(),
"elapsed_minutes": int(elapsed.total_seconds() // 60),
}
+88
View File
@@ -0,0 +1,88 @@
"""
Хранилище учётных данных пользователей.
"""
import json
import asyncio
import logging
from pathlib import Path
from typing import Optional, Dict
logger = logging.getLogger(__name__)
class AuthStorage:
"""Хранилище паролей пользователей в JSON-файле."""
def __init__(self, file_path: str = "data/users.json"):
self.file_path = Path(file_path)
self.file_path.parent.mkdir(parents=True, exist_ok=True)
self._lock = asyncio.Lock()
async def register(self, user_id: int, password_hash: str, salt: str) -> bool:
"""Регистрирует нового пользователя. Возвращает True если успешно."""
async with self._lock:
data = self._load()
if str(user_id) in data:
return False
data[str(user_id)] = {
"password_hash": password_hash,
"salt": salt,
}
self._save(data)
logger.info(f"User {user_id} registered")
return True
async def get_user(self, user_id: int) -> Optional[Dict]:
"""Получает данные пользователя по ID."""
async with self._lock:
data = self._load()
return data.get(str(user_id))
async def change_password(self, user_id: int, new_hash: str, new_salt: str) -> bool:
"""Сменить пароль пользователя."""
async with self._lock:
data = self._load()
user_str = str(user_id)
if user_str not in data:
return False
data[user_str]["password_hash"] = new_hash
data[user_str]["salt"] = new_salt
self._save(data)
logger.info(f"User {user_id} changed password")
return True
async def delete_user(self, user_id: int) -> bool:
"""Удалить учётную запись пользователя."""
async with self._lock:
data = self._load()
user_str = str(user_id)
if user_str not in data:
return False
del data[user_str]
self._save(data)
logger.info(f"User {user_id} deleted")
return True
async def user_exists(self, user_id: int) -> bool:
"""Проверяет, существует ли пользователь."""
async with self._lock:
data = self._load()
return str(user_id) in data
def _load(self) -> dict:
if not self.file_path.exists():
return {}
try:
with open(self.file_path, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"Failed to load auth storage: {e}")
return {}
def _save(self, data: dict):
try:
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except IOError as e:
logger.error(f"Failed to save auth storage: {e}")