Files
hive-manage-v3/backend/app/services/auth_service.py
T
2026-07-22 16:52:09 +08:00

97 lines
3.2 KiB
Python

"""Auth service: password hashing, JWT, user queries."""
from datetime import datetime, timedelta, timezone
from fastapi import HTTPException, status
from jose import JWTError, ExpiredSignatureError, 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, UserRole
_pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return _pwd.hash(password)
def verify_password(plain: str, hashed: str) -> bool:
return _pwd.verify(plain, hashed)
def create_access_token(user_id: str, role: str) -> str:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.jwt_expire_minutes)
payload = {"sub": user_id, "role": role, "exp": expire}
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
def decode_access_token(token: str) -> dict:
try:
return jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm])
except ExpiredSignatureError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
async def get_user_by_id(db: AsyncSession, user_id: str) -> User | None:
result = await db.execute(select(User).where(User.id == user_id))
return result.scalar_one_or_none()
async def get_user_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 authenticate_user(db: AsyncSession, username: str, password: str) -> User:
user = await get_user_by_username(db, username)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# Check lock
if user.locked_until and user.locked_until > datetime.utcnow():
raise HTTPException(
status_code=status.HTTP_423_LOCKED,
detail=f"Account locked until {user.locked_until.isoformat()}",
)
if not verify_password(password, user.hashed_password):
user.login_attempts += 1
if user.login_attempts >= 5:
user.locked_until = datetime.utcnow() + timedelta(minutes=15)
await db.commit()
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
# Success — reset
user.login_attempts = 0
user.locked_until = None
await db.commit()
return user
async def create_user(db: AsyncSession, username: str, email: str, password: str) -> User:
result = await db.execute(select(User))
existing_count = len(result.scalars().all())
if existing_count == 0:
role = UserRole.OWNER
is_approved = True
else:
role = UserRole.VIEWER
is_approved = False
user = User(
username=username,
email=email,
hashed_password=hash_password(password),
role=role,
is_approved=is_approved,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user