from datetime import datetime import re from fastapi import APIRouter, Depends, HTTPException, Request, status, UploadFile, File from fastapi.security import OAuth2PasswordRequestForm from pydantic import BaseModel from sqlalchemy.orm import Session from src.storage.db.manager import db_manager from src.storage.db.models import User from server.utils.auth_middleware import get_admin_user, get_current_user, get_db, get_required_user from server.utils.auth_utils import AuthUtils from server.utils.user_utils import generate_unique_user_id, validate_username, is_valid_phone_number from server.utils.common_utils import log_operation from src.storage.minio import upload_image_to_minio # 创建路由器 auth = APIRouter(prefix="/auth", tags=["authentication"]) # 请求和响应模型 class Token(BaseModel): access_token: str token_type: str user_id: int username: str user_id_login: str # 用于登录的user_id phone_number: str | None = None avatar: str | None = None role: str class UserCreate(BaseModel): username: str password: str role: str = "user" phone_number: str | None = None class UserUpdate(BaseModel): username: str | None = None password: str | None = None role: str | None = None phone_number: str | None = None avatar: str | None = None class UserProfileUpdate(BaseModel): username: str | None = None phone_number: str | None = None class UserResponse(BaseModel): id: int username: str user_id: str phone_number: str | None = None avatar: str | None = None role: str created_at: str last_login: str | None = None class InitializeAdmin(BaseModel): user_id: str # 直接输入用户ID password: str phone_number: str | None = None class UsernameValidation(BaseModel): username: str class UserIdGeneration(BaseModel): username: str user_id: str is_available: bool # ============================================================================= # === 工具函数 === # ============================================================================= # 路由:登录获取令牌 # ============================================================================= # === 认证分组 === # ============================================================================= @auth.post("/token", response_model=Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): # 查找用户 - 支持user_id和phone_number登录 login_identifier = form_data.username # OAuth2表单中的username字段作为登录标识符 # 尝试通过user_id查找 user = db.query(User).filter(User.user_id == login_identifier).first() # 如果通过user_id没找到,尝试通过phone_number查找 if not user: user = db.query(User).filter(User.phone_number == login_identifier).first() # 如果用户不存在,为防止用户名枚举攻击,返回通用错误信息 if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="登录标识或密码错误", headers={"WWW-Authenticate": "Bearer"}, ) # 检查用户是否处于登录锁定状态 if user.is_login_locked(): remaining_time = user.get_remaining_lock_time() raise HTTPException( status_code=status.HTTP_423_LOCKED, detail=f"登录被锁定,请等待 {remaining_time} 秒后再试", headers={"WWW-Authenticate": "Bearer", "X-Lock-Remaining": str(remaining_time)}, ) # 验证密码 if not AuthUtils.verify_password(user.password_hash, form_data.password): # 密码错误,增加失败次数 user.increment_failed_login() db.commit() # 记录失败操作 log_operation(db, user.id if user else None, "登录失败", f"密码错误,失败次数: {user.login_failed_count}") # 检查是否需要锁定 if user.is_login_locked(): remaining_time = user.get_remaining_lock_time() raise HTTPException( status_code=status.HTTP_423_LOCKED, detail=f"由于多次登录失败,账户已被锁定 {remaining_time} 秒", headers={"WWW-Authenticate": "Bearer", "X-Lock-Remaining": str(remaining_time)}, ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", headers={"WWW-Authenticate": "Bearer"}, ) # 登录成功,重置失败计数器 user.reset_failed_login() user.last_login = datetime.now() db.commit() # 生成访问令牌 token_data = {"sub": str(user.id)} access_token = AuthUtils.create_access_token(token_data) # 记录登录操作 log_operation(db, user.id, "登录") return { "access_token": access_token, "token_type": "bearer", "user_id": user.id, "username": user.username, "user_id_login": user.user_id, "phone_number": user.phone_number, "avatar": user.avatar, "role": user.role, } # 路由:校验是否需要初始化管理员 @auth.get("/check-first-run") async def check_first_run(): is_first_run = db_manager.check_first_run() return {"first_run": is_first_run} # 路由:初始化管理员账户 @auth.post("/initialize", response_model=Token) async def initialize_admin(admin_data: InitializeAdmin, db: Session = Depends(get_db)): # 检查是否是首次运行 if not db_manager.check_first_run(): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="系统已经初始化,无法再次创建初始管理员", ) # 创建管理员账户 hashed_password = AuthUtils.hash_password(admin_data.password) # 验证用户ID格式(只支持字母数字和下划线) if not re.match(r"^[a-zA-Z0-9_]+$", admin_data.user_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户ID只能包含字母、数字和下划线", ) if len(admin_data.user_id) < 3 or len(admin_data.user_id) > 20: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户ID长度必须在3-20个字符之间", ) # 验证手机号格式(如果提供了) if admin_data.phone_number and not is_valid_phone_number(admin_data.phone_number): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确") # 由于是首次初始化,直接使用输入的user_id user_id = admin_data.user_id new_admin = User( username=admin_data.user_id, # username和user_id设置为相同值 user_id=user_id, phone_number=admin_data.phone_number, avatar=None, # 初始化时头像为空 password_hash=hashed_password, role="superadmin", last_login=datetime.now(), ) db.add(new_admin) db.commit() db.refresh(new_admin) # 生成访问令牌 token_data = {"sub": str(new_admin.id)} access_token = AuthUtils.create_access_token(token_data) # 记录操作 log_operation(db, new_admin.id, "系统初始化", "创建超级管理员账户") return { "access_token": access_token, "token_type": "bearer", "user_id": new_admin.id, "username": new_admin.username, "user_id_login": new_admin.user_id, "phone_number": new_admin.phone_number, "avatar": new_admin.avatar, "role": new_admin.role, } # 路由:获取当前用户信息 # ============================================================================= # === 用户信息分组 === # ============================================================================= @auth.get("/me", response_model=UserResponse) async def read_users_me(current_user: User = Depends(get_current_user)): return current_user.to_dict() # 路由:更新个人资料 @auth.put("/profile", response_model=UserResponse) async def update_profile( profile_data: UserProfileUpdate, request: Request, current_user: User = Depends(get_required_user), db: Session = Depends(get_db), ): """更新当前用户的个人资料""" update_details = [] # 更新用户名(仅允许修改显示名,不修改 user_id) if profile_data.username is not None: # 验证用户名格式 is_valid, error_msg = validate_username(profile_data.username) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg, ) # 检查用户名是否已被其他用户使用 existing_user = ( db.query(User).filter(User.username == profile_data.username, User.id != current_user.id).first() ) if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在", ) current_user.username = profile_data.username update_details.append(f"用户名: {profile_data.username}") # 更新手机号 if profile_data.phone_number is not None: # 如果手机号不为空,验证格式 if profile_data.phone_number and not is_valid_phone_number(profile_data.phone_number): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号格式不正确") # 检查手机号是否已被其他用户使用 if profile_data.phone_number: existing_phone = ( db.query(User) .filter(User.phone_number == profile_data.phone_number, User.id != current_user.id) .first() ) if existing_phone: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号已被其他用户使用") current_user.phone_number = profile_data.phone_number update_details.append(f"手机号: {profile_data.phone_number or '已清空'}") db.commit() # 记录操作 if update_details: log_operation(db, current_user.id, "更新个人资料", f"更新个人资料: {', '.join(update_details)}", request) return current_user.to_dict() # 路由:创建新用户(管理员权限) # ============================================================================= # === 用户管理分组 === # ============================================================================= @auth.post("/users", response_model=UserResponse) async def create_user( user_data: UserCreate, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): # 验证用户名 is_valid, error_msg = validate_username(user_data.username) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg, ) # 检查用户名是否已存在 existing_user = db.query(User).filter(User.username == user_data.username).first() if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在", ) # 检查手机号是否已存在(如果提供了) if user_data.phone_number: existing_phone = db.query(User).filter(User.phone_number == user_data.phone_number).first() if existing_phone: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="手机号已存在", ) # 生成唯一的user_id existing_user_ids = [user.user_id for user in db.query(User.user_id).all()] user_id = generate_unique_user_id(user_data.username, existing_user_ids) # 创建新用户 hashed_password = AuthUtils.hash_password(user_data.password) # 检查角色权限 # 超级管理员可以创建任何类型的用户 if user_data.role == "superadmin" and current_user.role != "superadmin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="只有超级管理员才能创建超级管理员账户", ) # 管理员只能创建普通用户 if current_user.role == "admin" and user_data.role != "user": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="管理员只能创建普通用户账户", ) new_user = User( username=user_data.username, user_id=user_id, phone_number=user_data.phone_number, password_hash=hashed_password, role=user_data.role, ) db.add(new_user) db.commit() db.refresh(new_user) # 记录操作 log_operation(db, current_user.id, "创建用户", f"创建用户: {user_data.username}, 角色: {user_data.role}", request) return new_user.to_dict() # 路由:获取所有用户(管理员权限) @auth.get("/users", response_model=list[UserResponse]) async def read_users( skip: int = 0, limit: int = 100, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): users = db.query(User).offset(skip).limit(limit).all() return [user.to_dict() for user in users] # 路由:获取特定用户信息(管理员权限) @auth.get("/users/{user_id}", response_model=UserResponse) async def read_user(user_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)): user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在", ) return user.to_dict() # 路由:更新用户信息(管理员权限) @auth.put("/users/{user_id}", response_model=UserResponse) async def update_user( user_id: int, user_data: UserUpdate, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在", ) # 检查权限 if user.role == "superadmin" and current_user.role != "superadmin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="只有超级管理员才能修改超级管理员账户", ) # 超级管理员账户不能被降级(只能由其他超级管理员修改) if user.role == "superadmin" and user_data.role and user_data.role != "superadmin" and current_user.id != user.id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="不能降级超级管理员账户", ) # 更新信息 update_details = [] if user_data.username is not None: # 检查用户名是否已被其他用户使用 existing_user = db.query(User).filter(User.username == user_data.username, User.id != user_id).first() if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在", ) user.username = user_data.username update_details.append(f"用户名: {user_data.username}") if user_data.password is not None: user.password_hash = AuthUtils.hash_password(user_data.password) update_details.append("密码已更新") if user_data.role is not None: user.role = user_data.role update_details.append(f"角色: {user_data.role}") db.commit() # 记录操作 log_operation(db, current_user.id, "更新用户", f"更新用户ID {user_id}: {', '.join(update_details)}", request) return user.to_dict() # 路由:删除用户(管理员权限) @auth.delete("/users/{user_id}", response_model=dict) async def delete_user( user_id: int, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): user = db.query(User).filter(User.id == user_id).first() if user is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在", ) # 检查权限 if user.role == "superadmin": # 只有超级管理员可以删除超级管理员 if current_user.role != "superadmin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="只有超级管理员才能删除超级管理员账户", ) # 检查是否是最后一个超级管理员 superadmin_count = db.query(User).filter(User.role == "superadmin").count() if superadmin_count <= 1: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="不能删除最后一个超级管理员账户", ) # 不能删除自己的账户 if user.id == current_user.id: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="不能删除自己的账户", ) # 记录操作 log_operation( db, current_user.id, "删除用户", f"删除用户: {user.username}, ID: {user.id}, 角色: {user.role}", request ) # 删除用户 db.delete(user) db.commit() return {"success": True, "message": "用户已删除"} # 路由:验证用户名并生成user_id @auth.post("/validate-username", response_model=UserIdGeneration) async def validate_username_and_generate_user_id( validation_data: UsernameValidation, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """验证用户名格式并生成可用的user_id""" # 验证用户名格式 is_valid, error_msg = validate_username(validation_data.username) if not is_valid: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg, ) # 检查用户名是否已存在 existing_user = db.query(User).filter(User.username == validation_data.username).first() if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="用户名已存在", ) # 生成唯一的user_id existing_user_ids = [user.user_id for user in db.query(User.user_id).all()] user_id = generate_unique_user_id(validation_data.username, existing_user_ids) return UserIdGeneration(username=validation_data.username, user_id=user_id, is_available=True) # 路由:检查user_id是否可用 @auth.get("/check-user-id/{user_id}") async def check_user_id_availability( user_id: str, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db) ): """检查user_id是否可用""" existing_user = db.query(User).filter(User.user_id == user_id).first() return {"user_id": user_id, "is_available": existing_user is None} # 路由:上传用户头像 @auth.post("/upload-avatar") async def upload_user_avatar( file: UploadFile = File(...), current_user: User = Depends(get_required_user), db: Session = Depends(get_db) ): """上传用户头像""" # 检查文件类型 if not file.content_type or not file.content_type.startswith("image/"): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="只能上传图片文件") # 检查文件大小(5MB限制) file_size = 0 file_content = await file.read() file_size = len(file_content) if file_size > 5 * 1024 * 1024: # 5MB raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="文件大小不能超过5MB") try: # 获取文件扩展名 file_extension = file.filename.split(".")[-1].lower() if file.filename and "." in file.filename else "jpg" # 上传到MinIO avatar_url = upload_image_to_minio(file_content, file_extension) # 更新用户头像 current_user.avatar = avatar_url db.commit() # 记录操作 log_operation(db, current_user.id, "上传头像", f"更新头像: {avatar_url}") return {"success": True, "avatar_url": avatar_url, "message": "头像上传成功"} except Exception as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"头像上传失败: {str(e)}")