from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.security import hash_password, verify_password from app.models.role import Role from app.models.user import AUTH_SOURCE_LDAP, AUTH_SOURCE_LOCAL, User async def get(db: AsyncSession, user_id: int) -> User | None: return await db.get(User, user_id) async def get_by_email(db: AsyncSession, email: str) -> User | None: result = await db.execute(select(User).where(User.email == email)) return result.scalar_one_or_none() async def get_by_username(db: AsyncSession, username: str) -> User | None: result = await db.execute(select(User).where(User.username == username)) return result.scalar_one_or_none() async def list_users(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[User]: result = await db.execute(select(User).order_by(User.id).offset(skip).limit(limit)) return list(result.scalars().all()) async def count(db: AsyncSession) -> int: result = await db.execute(select(func.count()).select_from(User)) return result.scalar_one() async def _resolve_roles(db: AsyncSession, role_ids: list[int]) -> list[Role]: if not role_ids: return [] result = await db.execute(select(Role).where(Role.id.in_(role_ids))) return list(result.scalars().all()) async def create( db: AsyncSession, *, email: str, password: str | None = None, full_name: str | None = None, is_superuser: bool = False, role_ids: list[int] | None = None, username: str | None = None, auth_source: str = AUTH_SOURCE_LOCAL, ) -> User: user = User( email=email, username=username, hashed_password=hash_password(password) if password else None, auth_source=auth_source, full_name=full_name, is_superuser=is_superuser, roles=await _resolve_roles(db, role_ids or []), ) db.add(user) await db.commit() await db.refresh(user) return user async def get_or_create_ldap_user( db: AsyncSession, *, username: str, email: str, full_name: str | None, role_ids: list[int] | None = None, ) -> User: """Find an LDAP user by username, provisioning a local profile on first login. The profile is keyed on the LDAP username; email is synthesized by the caller. Newly provisioned users get ``role_ids`` (the default role). """ user = await get_by_username(db, username) if user is not None: # Keep the display name in sync with LDAP on subsequent logins. if full_name and user.full_name != full_name: user.full_name = full_name await db.commit() await db.refresh(user) return user return await create( db, email=email, username=username, full_name=full_name, auth_source=AUTH_SOURCE_LDAP, role_ids=role_ids, ) async def update(db: AsyncSession, user: User, data: dict) -> User: if "password" in data and data["password"]: user.hashed_password = hash_password(data.pop("password")) else: data.pop("password", None) if "role_ids" in data and data["role_ids"] is not None: user.roles = await _resolve_roles(db, data.pop("role_ids")) else: data.pop("role_ids", None) for field, value in data.items(): if value is not None: setattr(user, field, value) await db.commit() await db.refresh(user) return user async def delete(db: AsyncSession, user: User) -> None: await db.delete(user) await db.commit() async def authenticate(db: AsyncSession, email: str, password: str) -> User | None: user = await get_by_email(db, email) # LDAP users have no local password and must sign in via LDAP. if not user or not user.hashed_password: return None if not verify_password(password, user.hashed_password): return None return user