"""认证服务。""" from datetime import UTC, datetime, timedelta from jose import JWTError, jwt from pwdlib import PasswordHash from pwdlib.hashers.argon2 import Argon2Hasher from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.errors import AppError from app.core.ids import new_user_id from app.models.rbac import Permission, RolePermission, UserRole from app.models.user import User _pwd = PasswordHash([Argon2Hasher()]) def hash_password(plain: str) -> str: return _pwd.hash(plain) def verify_password(plain: str, hash: str) -> bool: return _pwd.verify(plain, hash) def _jwt_secret() -> str: secret = settings.jwt_secret if not secret: raise AppError("service_unavailable", "JWT 未配置。") return secret def create_access_token(subject: str, extra: dict | None = None) -> str: secret = _jwt_secret() now = datetime.now(UTC) payload = { "sub": subject, "type": "access", "iat": now, "exp": now + timedelta(seconds=settings.access_token_ttl), **(extra or {}), } return jwt.encode(payload, secret, algorithm="HS256") def create_refresh_token(subject: str) -> str: secret = _jwt_secret() now = datetime.now(UTC) payload = { "sub": subject, "type": "refresh", "iat": now, "exp": now + timedelta(seconds=settings.refresh_token_ttl), } return jwt.encode(payload, secret, algorithm="HS256") def decode_token(token: str) -> dict: secret = _jwt_secret() try: return jwt.decode(token, secret, algorithms=["HS256"]) except JWTError: raise AppError("unauthorized", "Token 无效或已过期。") from None async def authenticate_user(db: AsyncSession, username: str, password: str) -> User: result = await db.execute(select(User).where(User.username == username)) user = result.scalar_one_or_none() if user is None: raise AppError("unauthorized", "用户名或密码错误。") if not user.is_active: raise AppError("unauthorized", "用户已被禁用。") if not verify_password(password, user.password_hash): raise AppError("unauthorized", "用户名或密码错误。") return user 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 create_user( db: AsyncSession, username: str, email: str, password: str, is_admin: bool = False, ) -> User: # 检查用户名和邮箱是否已使用 result = await db.execute( select(User).where((User.username == username) | (User.email == email)) ) existing = result.scalar_one_or_none() if existing is not None: if existing.username == username: raise AppError("conflict", "用户名已被注册。") raise AppError("conflict", "邮箱已被注册。") user = User( id=new_user_id(), username=username, email=email, password_hash=hash_password(password), is_active=True, is_admin=is_admin, ) db.add(user) await db.flush() return user async def get_user_permissions(db: AsyncSession, user_id: str) -> list[str]: """查询用户的所有权限(通过角色继承),返回 ``resource:action`` 字符串列表。""" result = await db.execute( select(Permission) .join(RolePermission, Permission.id == RolePermission.permission_id) .join(UserRole, RolePermission.role_id == UserRole.role_id) .where(UserRole.user_id == user_id) ) return [p.code for p in result.scalars().all()]