初步实现数据加密、权限管理、日志审计等功能

This commit is contained in:
2026-02-02 10:09:28 +08:00
parent b6b37a453b
commit 807e634318
27 changed files with 3787 additions and 59 deletions

View File

@@ -0,0 +1,99 @@
"""
审计日志 API 接口
仅管理员可访问
"""
from typing import List, Optional
from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request
from app.domain.schemas.audit import AuditLogResponse, AuditLogQuery
from app.domain.schemas.user import UserInDB
from app.infra.repositories.audit_repository import AuditRepository
from app.auth.dependencies import get_user_repository, get_db
from app.auth.permissions import get_current_admin
from app.infra.db.postgresql.database import Database
router = APIRouter()
async def get_audit_repository(db: Database = Depends(get_db)) -> AuditRepository:
"""获取审计日志仓储"""
return AuditRepository(db)
@router.get("/logs", response_model=List[AuditLogResponse])
async def get_audit_logs(
user_id: Optional[int] = Query(None, description="按用户ID过滤"),
username: Optional[str] = Query(None, description="按用户名过滤"),
action: Optional[str] = Query(None, description="按操作类型过滤"),
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
start_time: Optional[datetime] = Query(None, description="开始时间"),
end_time: Optional[datetime] = Query(None, description="结束时间"),
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=1000, description="限制记录数"),
current_user: UserInDB = Depends(get_current_admin),
audit_repo: AuditRepository = Depends(get_audit_repository)
) -> List[AuditLogResponse]:
"""
查询审计日志(仅管理员)
支持按用户、时间、操作类型等条件过滤
"""
logs = await audit_repo.get_logs(
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
start_time=start_time,
end_time=end_time,
skip=skip,
limit=limit
)
return logs
@router.get("/logs/count")
async def get_audit_logs_count(
user_id: Optional[int] = Query(None, description="按用户ID过滤"),
username: Optional[str] = Query(None, description="按用户名过滤"),
action: Optional[str] = Query(None, description="按操作类型过滤"),
resource_type: Optional[str] = Query(None, description="按资源类型过滤"),
start_time: Optional[datetime] = Query(None, description="开始时间"),
end_time: Optional[datetime] = Query(None, description="结束时间"),
current_user: UserInDB = Depends(get_current_admin),
audit_repo: AuditRepository = Depends(get_audit_repository)
) -> dict:
"""
获取审计日志总数(仅管理员)
"""
count = await audit_repo.get_log_count(
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
start_time=start_time,
end_time=end_time
)
return {"count": count}
@router.get("/logs/my", response_model=List[AuditLogResponse])
async def get_my_audit_logs(
action: Optional[str] = Query(None, description="按操作类型过滤"),
start_time: Optional[datetime] = Query(None, description="开始时间"),
end_time: Optional[datetime] = Query(None, description="结束时间"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
current_user: UserInDB = Depends(get_current_admin),
audit_repo: AuditRepository = Depends(get_audit_repository)
) -> List[AuditLogResponse]:
"""
查询当前用户的审计日志
普通用户只能查看自己的操作记录
"""
logs = await audit_repo.get_logs(
user_id=current_user.id,
action=action,
start_time=start_time,
end_time=end_time,
skip=skip,
limit=limit
)
return logs

View File

@@ -1,52 +1,186 @@
from typing import Annotated, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Header, status
from pydantic import BaseModel
from typing import Annotated
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.core.config import settings
from app.core.security import create_access_token, create_refresh_token, verify_password
from app.domain.schemas.user import UserCreate, UserResponse, UserLogin, Token
from app.infra.repositories.user_repository import UserRepository
from app.auth.dependencies import get_user_repository, get_current_active_user
from app.domain.schemas.user import UserInDB
import logging
logger = logging.getLogger(__name__)
router = APIRouter()
# 简易令牌验证(实际项目中应替换为 JWT/OAuth2 等)
AUTH_TOKEN = "567e33c876a2" # 预设的有效令牌
WHITE_LIST = ["/docs", "/openapi.json", "/redoc", "/api/v1/auth/login/"]
async def verify_token(authorization: Annotated[str, Header()] = None):
# 检查请求头是否存在
if not authorization:
raise HTTPException(status_code=401, detail="Authorization header missing")
# 提取 Bearer 后的令牌 (格式: Bearer <token>)
try:
token_type, token = authorization.split(" ", 1)
if token_type.lower() != "bearer":
raise ValueError
except ValueError:
@router.post("/register", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def register(
user_data: UserCreate,
user_repo: UserRepository = Depends(get_user_repository)
) -> UserResponse:
"""
用户注册
创建新用户账号
"""
# 检查用户名和邮箱是否已存在
if await user_repo.user_exists(username=user_data.username):
raise HTTPException(
status_code=401, detail="Invalid authorization format. Use: Bearer <token>"
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already registered"
)
if await user_repo.user_exists(email=user_data.email):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# 创建用户
try:
user = await user_repo.create_user(user_data)
if not user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user"
)
return UserResponse.model_validate(user)
except Exception as e:
logger.error(f"Error during user registration: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
# 验证令牌
if token != AUTH_TOKEN:
raise HTTPException(status_code=403, detail="Invalid authentication token")
return True
def generate_access_token(username: str, password: str) -> str:
@router.post("/login", response_model=Token)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
user_repo: UserRepository = Depends(get_user_repository)
) -> Token:
"""
根据用户名和密码生成JWT access token
参数:
username: 用户名
password: 密码
返回:
JWT access token字符串
用户登录OAuth2 标准格式)
返回 JWT Access Token 和 Refresh Token
"""
# 验证用户(支持用户名或邮箱登录)
user = await user_repo.get_user_by_username(form_data.username)
if not user:
# 尝试用邮箱登录
user = await user_repo.get_user_by_email(form_data.username)
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user account"
)
# 生成 Token
access_token = create_access_token(subject=user.username)
refresh_token = create_refresh_token(subject=user.username)
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
if username != "tjwater" or password != "tjwater@123":
raise ValueError("用户名或密码错误")
@router.post("/login/simple", response_model=Token)
async def login_simple(
username: str,
password: str,
user_repo: UserRepository = Depends(get_user_repository)
) -> Token:
"""
简化版登录接口(保持向后兼容)
直接使用 username 和 password 参数
"""
# 验证用户
user = await user_repo.get_user_by_username(username)
if not user:
user = await user_repo.get_user_by_email(username)
if not user or not verify_password(password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Inactive user account"
)
# 生成 Token
access_token = create_access_token(subject=user.username)
refresh_token = create_refresh_token(subject=user.username)
return Token(
access_token=access_token,
refresh_token=refresh_token,
token_type="bearer",
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
token = "567e33c876a2"
return token
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: UserInDB = Depends(get_current_active_user)
) -> UserResponse:
"""
获取当前登录用户信息
"""
return UserResponse.model_validate(current_user)
@router.post("/login/")
async def login(username: str, password: str) -> str:
return generate_access_token(username, password)
@router.post("/refresh", response_model=Token)
async def refresh_token(
refresh_token: str,
user_repo: UserRepository = Depends(get_user_repository)
) -> Token:
"""
刷新 Access Token
使用 Refresh Token 获取新的 Access Token
"""
from jose import jwt, JWTError
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(refresh_token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
username: str = payload.get("sub")
token_type: str = payload.get("type")
if username is None or token_type != "refresh":
raise credentials_exception
except JWTError:
raise credentials_exception
# 验证用户仍然存在且激活
user = await user_repo.get_user_by_username(username)
if not user or not user.is_active:
raise credentials_exception
# 生成新的 Access Token
new_access_token = create_access_token(subject=user.username)
return Token(
access_token=new_access_token,
refresh_token=refresh_token, # 保持原 refresh token
token_type="bearer",
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
)

View File

@@ -0,0 +1,180 @@
"""
用户管理 API 接口
演示权限控制的使用
"""
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from app.domain.schemas.user import UserResponse, UserUpdate, UserCreate
from app.domain.models.role import UserRole
from app.domain.schemas.user import UserInDB
from app.infra.repositories.user_repository import UserRepository
from app.auth.dependencies import get_user_repository, get_current_active_user
from app.auth.permissions import get_current_admin, require_role, check_resource_owner
router = APIRouter()
@router.get("/", response_model=List[UserResponse])
async def list_users(
skip: int = 0,
limit: int = 100,
current_user: UserInDB = Depends(require_role(UserRole.ADMIN)),
user_repo: UserRepository = Depends(get_user_repository)
) -> List[UserResponse]:
"""
获取用户列表(仅管理员)
"""
users = await user_repo.get_all_users(skip=skip, limit=limit)
return [UserResponse.model_validate(user) for user in users]
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
current_user: UserInDB = Depends(get_current_active_user),
user_repo: UserRepository = Depends(get_user_repository)
) -> UserResponse:
"""
获取用户详情
管理员可查看所有用户,普通用户只能查看自己
"""
# 检查权限
if not check_resource_owner(user_id, current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to view this user"
)
user = await user_repo.get_user_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse.model_validate(user)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_update: UserUpdate,
current_user: UserInDB = Depends(get_current_active_user),
user_repo: UserRepository = Depends(get_user_repository)
) -> UserResponse:
"""
更新用户信息
管理员可更新所有用户,普通用户只能更新自己(且不能修改角色)
"""
# 检查用户是否存在
target_user = await user_repo.get_user_by_id(user_id)
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
# 权限检查
is_owner = current_user.id == user_id
is_admin = UserRole(current_user.role).has_permission(UserRole.ADMIN)
if not is_owner and not is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to update this user"
)
# 非管理员不能修改角色和激活状态
if not is_admin:
if user_update.role is not None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can change user roles"
)
if user_update.is_active is not None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only admins can change user active status"
)
# 更新用户
updated_user = await user_repo.update_user(user_id, user_update)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user"
)
return UserResponse.model_validate(updated_user)
@router.delete("/{user_id}")
async def delete_user(
user_id: int,
current_user: UserInDB = Depends(get_current_admin),
user_repo: UserRepository = Depends(get_user_repository)
) -> dict:
"""
删除用户(仅管理员)
"""
# 不能删除自己
if current_user.id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot delete your own account"
)
success = await user_repo.delete_user(user_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"message": "User deleted successfully"}
@router.post("/{user_id}/activate")
async def activate_user(
user_id: int,
current_user: UserInDB = Depends(get_current_admin),
user_repo: UserRepository = Depends(get_user_repository)
) -> UserResponse:
"""
激活用户(仅管理员)
"""
user_update = UserUpdate(is_active=True)
updated_user = await user_repo.update_user(user_id, user_update)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse.model_validate(updated_user)
@router.post("/{user_id}/deactivate")
async def deactivate_user(
user_id: int,
current_user: UserInDB = Depends(get_current_admin),
user_repo: UserRepository = Depends(get_user_repository)
) -> UserResponse:
"""
停用用户(仅管理员)
"""
# 不能停用自己
if current_user.id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You cannot deactivate your own account"
)
user_update = UserUpdate(is_active=False)
updated_user = await user_repo.update_user(user_id, user_update)
if not updated_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return UserResponse.model_validate(updated_user)

View File

@@ -12,6 +12,8 @@ from app.api.v1.endpoints import (
misc,
risk,
cache,
user_management, # 新增:用户管理
audit, # 新增:审计日志
)
from app.api.v1.endpoints.network import (
general,
@@ -42,6 +44,8 @@ api_router = APIRouter()
# Core Services
api_router.include_router(auth.router, tags=["Auth"])
api_router.include_router(user_management.router, prefix="/users", tags=["User Management"]) # 新增
api_router.include_router(audit.router, prefix="/audit", tags=["Audit Logs"]) # 新增
api_router.include_router(project.router, tags=["Project"])
# Network Elements (Node/Link Types)