Files
hunjing/backend/app/services/auth.py
T
2026-08-21 16:00:13 +08:00

124 lines
3.7 KiB
Python

"""认证服务。"""
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()]