初步实现数据加密、权限管理、日志审计等功能
This commit is contained in:
99
app/api/v1/endpoints/audit.py
Normal file
99
app/api/v1/endpoints/audit.py
Normal 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
|
||||
@@ -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
|
||||
)
|
||||
|
||||
180
app/api/v1/endpoints/user_management.py
Normal file
180
app/api/v1/endpoints/user_management.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user