"""Auth service — JWT token handling + password hashing.""" from datetime import datetime, timedelta, timezone from jose import JWTError, jwt from passlib.context import CryptContext from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.models.user import User pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_DAYS = 7 def hash_password(password: str) -> str: return pwd_context.hash(password) def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed) def create_access_token(user_id: int, username: str) -> str: expire = datetime.now(timezone.utc) + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) payload = {"sub": str(user_id), "username": username, "exp": expire} return jwt.encode(payload, settings.SECRET_KEY, algorithm=ALGORITHM) def decode_access_token(token: str) -> dict | None: try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM]) return payload except JWTError: return None class AuthService: def __init__(self, db: AsyncSession): self.db = db async def authenticate(self, username: str, password: str) -> User | None: stmt = select(User).where(User.username == username, User.is_active == True) result = await self.db.execute(stmt) user = result.scalar_one_or_none() if user and verify_password(password, user.password_hash): return user return None async def get_user_by_id(self, user_id: int) -> User | None: return await self.db.get(User, user_id) @staticmethod async def ensure_default_admin(db: AsyncSession): """Create default admin if no users exist.""" stmt = select(User).limit(1) result = await db.execute(stmt) if result.scalar_one_or_none() is None: admin = User( username="admin", password_hash=hash_password("admin123"), display_name="Administrator", email="admin@taskpulse.local", ) db.add(admin) await db.flush() await db.commit()