nex_docus/backend/app/api/v1/auth.py

210 lines
6.7 KiB
Python
Raw Normal View History

2025-12-20 11:18:59 +00:00
"""
用户认证相关 API
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from datetime import datetime
import logging
from app.core.database import get_db
from app.core.security import verify_password, get_password_hash, create_access_token
from app.core.deps import get_current_user
from app.core.redis_client import TokenCache
from app.models.user import User
from app.models.role import Role, UserRole
from app.schemas.user import UserCreate, UserLogin, UserResponse, Token, ChangePassword, UserUpdate
from app.schemas.response import success_response, error_response
2025-12-29 12:53:50 +00:00
from app.services.log_service import log_service
from app.core.enums import OperationType, ResourceType
2025-12-20 11:18:59 +00:00
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/register", response_model=dict)
2025-12-29 12:53:50 +00:00
async def register(
user_in: UserCreate,
request: Request,
db: AsyncSession = Depends(get_db)
):
2025-12-20 11:18:59 +00:00
"""用户注册"""
# 检查用户名是否存在
result = await db.execute(select(User).where(User.username == user_in.username))
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(status_code=400, detail="用户名已存在")
# 检查邮箱是否存在
if user_in.email:
result = await db.execute(select(User).where(User.email == user_in.email))
existing_email = result.scalar_one_or_none()
if existing_email:
raise HTTPException(status_code=400, detail="邮箱已被注册")
# 创建用户
db_user = User(
username=user_in.username,
password_hash=get_password_hash(user_in.password),
nickname=user_in.nickname or user_in.username,
email=user_in.email,
phone=user_in.phone,
status=1,
)
db.add(db_user)
await db.commit()
await db.refresh(db_user)
# 分配默认角色(普通用户)
result = await db.execute(select(Role).where(Role.role_code == "user"))
default_role = result.scalar_one_or_none()
if default_role:
user_role = UserRole(user_id=db_user.id, role_id=default_role.id)
db.add(user_role)
await db.commit()
2025-12-29 12:53:50 +00:00
# 记录注册日志
await log_service.log_operation(
db=db,
operation_type=OperationType.USER_REGISTER,
resource_type=ResourceType.USER,
user=db_user,
resource_id=db_user.id,
detail={"username": db_user.username, "email": user_in.email},
request=request,
)
2025-12-20 11:18:59 +00:00
return success_response(
data={"user_id": db_user.id, "username": db_user.username},
message="注册成功"
)
@router.post("/login", response_model=dict)
2025-12-29 12:53:50 +00:00
async def login(
user_in: UserLogin,
request: Request,
db: AsyncSession = Depends(get_db)
):
2025-12-20 11:18:59 +00:00
"""用户登录"""
# 查询用户
result = await db.execute(select(User).where(User.username == user_in.username))
user = result.scalar_one_or_none()
if not user or not verify_password(user_in.password, user.password_hash):
raise HTTPException(status_code=401, detail="用户名或密码错误")
if user.status != 1:
raise HTTPException(status_code=403, detail="用户已被禁用")
# 更新最后登录时间
user.last_login_at = datetime.utcnow()
await db.commit()
# 生成 Tokensub 必须是字符串)
access_token = create_access_token(data={"sub": str(user.id)})
# 保存 token 到 Redis24小时过期
await TokenCache.save_token(user.id, access_token, expire_seconds=86400)
2025-12-29 12:53:50 +00:00
# 记录登录日志
await log_service.log_operation(
db=db,
operation_type=OperationType.USER_LOGIN,
resource_type=ResourceType.USER,
user=user,
resource_id=user.id,
detail={"username": user.username},
request=request,
)
2025-12-20 11:18:59 +00:00
# 返回用户信息和 Token
user_data = UserResponse.from_orm(user)
token_data = Token(access_token=access_token, user=user_data)
return success_response(data=token_data.dict(), message="登录成功")
@router.get("/me", response_model=dict)
async def get_current_user_info(current_user: User = Depends(get_current_user)):
"""获取当前用户信息"""
user_data = UserResponse.from_orm(current_user)
return success_response(data=user_data.dict())
@router.put("/profile", response_model=dict)
async def update_profile(
profile_in: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""更新用户资料"""
# 检查邮箱是否已被其他用户使用
if profile_in.email:
result = await db.execute(
select(User).where(User.email == profile_in.email, User.id != current_user.id)
)
existing_email = result.scalar_one_or_none()
if existing_email:
raise HTTPException(status_code=400, detail="邮箱已被其他用户使用")
# 更新字段
update_data = profile_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(current_user, field, value)
await db.commit()
await db.refresh(current_user)
user_data = UserResponse.from_orm(current_user)
return success_response(data=user_data.dict(), message="资料更新成功")
@router.post("/change-password", response_model=dict)
async def change_password(
password_in: ChangePassword,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
"""修改密码"""
# 验证旧密码
if not verify_password(password_in.old_password, current_user.password_hash):
raise HTTPException(status_code=400, detail="旧密码错误")
# 更新密码
current_user.password_hash = get_password_hash(password_in.new_password)
await db.commit()
# 密码修改后,删除用户所有 token强制重新登录
await TokenCache.delete_user_all_tokens(current_user.id)
return success_response(message="密码修改成功,请重新登录")
@router.post("/logout", response_model=dict)
async def logout(
request: Request,
2025-12-29 12:53:50 +00:00
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
2025-12-20 11:18:59 +00:00
):
"""退出登录"""
# 从请求状态中获取 token已在 get_current_user 中保存)
token = getattr(request.state, 'token', None)
if token:
await TokenCache.delete_token(token)
logger.info(f"User {current_user.username} logged out")
2025-12-29 12:53:50 +00:00
# 记录登出日志
await log_service.log_operation(
db=db,
operation_type=OperationType.USER_LOGOUT,
resource_type=ResourceType.USER,
user=current_user,
resource_id=current_user.id,
detail={"username": current_user.username},
request=request,
)
2025-12-20 11:18:59 +00:00
return success_response(message="退出成功")