"""Nexus — Admin & LoginAttempt Repository (Async SQLAlchemy)""" from typing import Optional from sqlalchemy import select, func, delete from sqlalchemy.ext.asyncio import AsyncSession from server.domain.models import Admin, LoginAttempt class AdminRepositoryImpl: def __init__(self, session: AsyncSession): self.session = session async def get_by_id(self, id: int) -> Optional[Admin]: result = await self.session.execute(select(Admin).where(Admin.id == id)) return result.scalar_one_or_none() async def get_by_username(self, username: str) -> Optional[Admin]: result = await self.session.execute(select(Admin).where(Admin.username == username)) return result.scalar_one_or_none() async def create(self, admin: Admin) -> Admin: self.session.add(admin) await self.session.commit() return admin async def update(self, admin: Admin) -> Admin: await self.session.commit() return admin class LoginAttemptRepositoryImpl: def __init__(self, session: AsyncSession): self.session = session async def create(self, attempt: LoginAttempt) -> LoginAttempt: self.session.add(attempt) await self.session.commit() return attempt async def count_recent_failures(self, username: str, ip_address: str, minutes: int = 15) -> int: from datetime import datetime, timedelta, timezone cutoff = datetime.now(timezone.utc) - timedelta(minutes=minutes) # Lock by username (primary) OR by IP (secondary) — whichever is over threshold. # Using IP-only for counting means an attacker can rotate IPs to bypass; # we count all IPs that attempted this username so rotating IPs doesn't help. result = await self.session.execute( select(func.count(LoginAttempt.id)) .where(LoginAttempt.username == username) .where(LoginAttempt.success == False) .where(LoginAttempt.attempted_at >= cutoff) ) return result.scalar_one() or 0 async def clear_failures_for_username(self, username: str) -> int: result = await self.session.execute( delete(LoginAttempt) .where(LoginAttempt.username == username) .where(LoginAttempt.success == False) ) await self.session.commit() return result.rowcount or 0