Files
site_aegisone/py_service/app/auth.py
T

91 lines
2.9 KiB
Python

import time
import secrets
import bcrypt
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.models.models import User, LoginAttempt, AuditLog
BRUTE_FORCE_WINDOW = 180
BRUTE_FORCE_MAX_ATTEMPTS = 3
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode()[:72], bcrypt.gensalt()).decode()
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode()[:72], hashed.encode())
def generate_csrf_token() -> str:
return secrets.token_hex(32)
async def brute_force_check(db: AsyncSession, login: str, ip_address: str) -> bool:
cutoff = datetime.utcnow().timestamp() - BRUTE_FORCE_WINDOW
result = await db.execute(
select(func.count(LoginAttempt.id))
.where(
(LoginAttempt.login == login) | (LoginAttempt.ip_address == ip_address),
LoginAttempt.attempt_time > datetime.fromtimestamp(cutoff),
)
)
count = result.scalar() or 0
return count >= BRUTE_FORCE_MAX_ATTEMPTS
async def record_attempt(db: AsyncSession, login: str, ip_address: str):
attempt = LoginAttempt(login=login, ip_address=ip_address)
db.add(attempt)
await db.flush()
async def clear_attempts(db: AsyncSession, login: str, ip_address: str):
from sqlalchemy import delete
await db.execute(
delete(LoginAttempt).where(
(LoginAttempt.login == login) | (LoginAttempt.ip_address == ip_address)
)
)
await db.flush()
async def authenticate(db: AsyncSession, login: str, password: str, ip_address: str) -> dict:
if await brute_force_check(db, login, ip_address):
return {"success": False, "error": "Превышен лимит попыток. Подождите 3 минуты."}
result = await db.execute(
select(User).where(User.login == login).limit(1)
)
user = result.scalar_one_or_none()
if not user or not verify_password(password, user.password_hash):
await record_attempt(db, login, ip_address)
return {"success": False, "error": "Неверный логин или пароль."}
if not user.is_active:
return {"success": False, "error": "Учётная запись заблокирована."}
await clear_attempts(db, login, ip_address)
await audit_log(db, user.id, "login", "Вход в систему", ip_address)
return {
"success": True,
"user": {
"id": user.id,
"login": user.login,
"role": user.role,
"full_name": user.full_name,
},
}
async def audit_log(db: AsyncSession, user_id: int | None, action: str, details: str, ip_address: str):
try:
log = AuditLog(user_id=user_id, action=action, details=details, ip_address=ip_address)
db.add(log)
await db.flush()
except Exception:
pass