Initial commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import time
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from passlib.context import CryptContext
|
||||
from app.models.models import User, LoginAttempt, AuditLog
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
BRUTE_FORCE_WINDOW = 180
|
||||
BRUTE_FORCE_MAX_ATTEMPTS = 3
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(password: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(password, hashed)
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user