Fix API compatibility and add user/role/permission and asset import/export

This commit is contained in:
2026-01-25 23:36:23 +08:00
commit 501d11e14e
371 changed files with 68853 additions and 0 deletions

View File

@@ -0,0 +1,285 @@
"""
认证服务
"""
from datetime import datetime, timedelta
from typing import Optional
from fastapi import status
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import security_manager
from app.core.exceptions import (
InvalidCredentialsException,
UserLockedException,
UserDisabledException,
CaptchaException
)
from app.crud.user import user_crud
from app.models.user import User
from app.schemas.user import LoginResponse, UserInfo
from app.core.config import settings
import uuid
class AuthService:
"""认证服务类"""
def __init__(self):
self.max_login_failures = 5
self.lock_duration_minutes = 30
async def login(
self,
db: AsyncSession,
username: str,
password: str,
captcha: str,
captcha_key: str
) -> LoginResponse:
"""
用户登录
Args:
db: 数据库会话
username: 用户名
password: 密码
captcha: 验证码
captcha_key: 验证码UUID
Returns:
LoginResponse: 登录响应
Raises:
InvalidCredentialsException: 认证失败
UserLockedException: 用户被锁定
UserDisabledException: 用户被禁用
"""
# 验证验证码
if not await self._verify_captcha(captcha_key, captcha):
raise CaptchaException()
# 获取用户
user = await user_crud.get_by_username(db, username)
if not user:
raise InvalidCredentialsException("用户名或密码错误")
# 检查用户状态 - 使用实际的数据库字段 is_active
if not user.is_active:
raise UserDisabledException()
# 验证密码 - 使用实际的数据库字段 hashed_password
if not security_manager.verify_password(password, user.hashed_password):
raise InvalidCredentialsException("用户名或密码错误")
# 登录成功,重置失败次数
await user_crud.update_last_login(db, user)
# 生成Token
access_token = security_manager.create_access_token(
data={"sub": str(user.id), "username": user.username}
)
refresh_token = security_manager.create_refresh_token(
data={"sub": str(user.id), "username": user.username}
)
# 获取用户角色和权限
user_info = await self._build_user_info(db, user)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token,
token_type="Bearer",
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
user=user_info
)
async def refresh_token(self, db: AsyncSession, refresh_token: str) -> dict:
"""
刷新访问令牌
Args:
db: 数据库会话
refresh_token: 刷新令牌
Returns:
dict: 包含新的访问令牌
"""
payload = security_manager.verify_token(refresh_token, token_type="refresh")
user_id = int(payload.get("sub"))
user = await user_crud.get(db, user_id)
if not user or not user.is_active:
raise InvalidCredentialsException("用户不存在或已被禁用")
# 生成新的访问令牌
access_token = security_manager.create_access_token(
data={"sub": str(user.id), "username": user.username}
)
return {
"access_token": access_token,
"expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
async def change_password(
self,
db: AsyncSession,
user: User,
old_password: str,
new_password: str
) -> bool:
"""
修改密码
Args:
db: 数据库会话
user: 当前用户
old_password: 旧密码
new_password: 新密码
Returns:
bool: 是否修改成功
"""
# 验证旧密码
if not security_manager.verify_password(old_password, user.hashed_password):
raise InvalidCredentialsException("旧密码错误")
# 更新密码
return await user_crud.update_password(db, user, new_password)
async def reset_password(
self,
db: AsyncSession,
user_id: int,
new_password: str
) -> bool:
"""
重置用户密码(管理员功能)
Args:
db: 数据库会话
user_id: 用户ID
new_password: 新密码
Returns:
bool: 是否重置成功
"""
user = await user_crud.get(db, user_id)
if not user:
return False
return await user_crud.update_password(db, user, new_password)
async def _generate_captcha(self) -> dict:
"""
生成验证码
Returns:
包含captcha_key和captcha_base64的字典
"""
from app.utils.redis_client import redis_client
import random
import string
import base64
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
# 生成4位随机验证码
captcha_text = ''.join(random.choices(string.digits, k=4))
# 生成验证码图片
width, height = 120, 40
image = Image.new('RGB', (width, height), color='white')
draw = ImageDraw.Draw(image)
# 尝试使用系统字体
font = None
font_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"arial.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
]
for font_path in font_paths:
try:
font = ImageFont.truetype(font_path, 28)
break
except:
continue
if font is None:
font = ImageFont.load_default(size=28)
# 绘制验证码
draw.text((10, 5), captcha_text, fill='black', font=font)
# 添加干扰线
for _ in range(5):
x1 = random.randint(0, width)
y1 = random.randint(0, height)
x2 = random.randint(0, width)
y2 = random.randint(0, height)
draw.line([(x1, y1), (x2, y2)], fill='gray', width=1)
# 转换为base64
buffer = BytesIO()
image.save(buffer, format='PNG')
image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
# 生成captcha_key
captcha_key = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
# 存储到Redis5分钟过期
await redis_client.setex(
f"captcha:{captcha_key}",
300,
captcha_text
)
return {
"captcha_key": captcha_key,
"captcha_base64": f"data:image/png;base64,{image_base64}"
}
async def _verify_captcha(self, captcha_key: str, captcha: str) -> bool:
"""
验证验证码
Args:
captcha_key: 验证码密钥
captcha: 用户输入的验证码
Returns:
验证是否成功
"""
from app.utils.redis_client import redis_client
# 从Redis获取存储的验证码
stored_captcha = await redis_client.get(f"captcha:{captcha_key}")
if not stored_captcha:
return False
# 验证码不区分大小写
return stored_captcha.lower() == captcha.lower()
async def _build_user_info(self, db: AsyncSession, user: User) -> UserInfo:
"""
构建用户信息
Args:
db: 数据库会话
user: 用户对象
Returns:
UserInfo: 用户信息
"""
return UserInfo(
id=user.id,
username=user.username,
real_name=user.full_name or user.username,
email=user.email,
avatar_url=user.avatar_url,
is_admin=user.is_superuser,
status="active" if user.is_active else "disabled"
)
# 创建服务实例
auth_service = AuthService()