fix(ipam): 修复IP管理页面网段筛选不生效及下拉框过窄
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄 - Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致) - Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效 - enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# app/core/__init__.py
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base, engine, get_db, SessionLocal
|
||||
|
||||
__all__ = ["settings", "Base", "engine", "get_db", "SessionLocal"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 应用配置
|
||||
APP_NAME: str = "IPAM Management System"
|
||||
APP_VERSION: str = "0.1.0"
|
||||
DEBUG: bool = True
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL: str = "mysql+pymysql://ipam:***@localhost:3308/ipam"
|
||||
|
||||
# Redis配置
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
# Celery配置
|
||||
CELERY_BROKER_URL: str = "redis://localhost:6379/1"
|
||||
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY: str = "your-super-secret-key-here-change-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7
|
||||
|
||||
# 扫描配置
|
||||
PING_TIMEOUT: int = 2
|
||||
PING_RETRIES: int = 2
|
||||
SCAN_CONCURRENCY: int = 50
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,31 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
pool_pre_ping=True,
|
||||
pool_recycle=3600,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
echo=settings.DEBUG
|
||||
)
|
||||
|
||||
# 创建会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 创建基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""
|
||||
获取数据库会话的依赖项
|
||||
"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,158 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.models.auth import User, UserStatus
|
||||
|
||||
# 密码加密上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# OAuth2 认证方案
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/v1/auth/login")
|
||||
|
||||
|
||||
class SecurityUtils:
|
||||
"""安全工具类"""
|
||||
|
||||
@staticmethod
|
||||
def hash_password(password: str) -> str:
|
||||
"""密码哈希加密"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
@staticmethod
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建访问令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
|
||||
to_encode.update({"exp": expire, "type": "access"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@staticmethod
|
||||
def create_refresh_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""创建刷新令牌"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(days=7) # 刷新令牌7天过期
|
||||
|
||||
to_encode.update({"exp": expire, "type": "refresh"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token: str, credentials_exception=None) -> Optional[dict]:
|
||||
"""验证令牌"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
token_type: str = payload.get("type")
|
||||
|
||||
if username is None:
|
||||
raise credentials_exception or HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
raise credentials_exception or HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
# 权限校验依赖
|
||||
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
|
||||
"""获取当前登录用户"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
payload = SecurityUtils.verify_token(token, credentials_exception)
|
||||
username: str = payload.get("sub")
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
|
||||
# 检查用户状态
|
||||
if user.status != UserStatus.ACTIVE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="用户已被禁用或锁定"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""获取当前活跃用户(已废弃,保留兼容)"""
|
||||
return current_user
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""权限检查器"""
|
||||
|
||||
def __init__(self, required_permission: str):
|
||||
self.required_permission = required_permission
|
||||
|
||||
def __call__(self, current_user: User = Depends(get_current_user)) -> User:
|
||||
"""检查用户是否拥有指定权限"""
|
||||
# 超级管理员拥有所有权限
|
||||
if current_user.role == "super_admin":
|
||||
return current_user
|
||||
|
||||
# 基于角色的权限检查
|
||||
role_permissions = {
|
||||
"admin": {
|
||||
"network", "ip", "scan", "snmp", "alert", "report"
|
||||
},
|
||||
"operator": {
|
||||
"network:view", "ip:view", "ip:edit", "scan:run", "alert:view"
|
||||
},
|
||||
"viewer": {
|
||||
"network:view", "ip:view", "alert:view"
|
||||
}
|
||||
}
|
||||
|
||||
user_permissions = role_permissions.get(current_user.role, set())
|
||||
|
||||
# 检查是否有通配符权限(如 "network" 包含 "network:create")
|
||||
permission_category = self.required_permission.split(":")[0]
|
||||
if (
|
||||
self.required_permission not in user_permissions and
|
||||
permission_category not in user_permissions and
|
||||
"*" not in user_permissions
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"缺少权限: {self.required_permission}"
|
||||
)
|
||||
|
||||
return current_user
|
||||
|
||||
|
||||
# 快捷权限检查依赖
|
||||
def require_permission(permission: str):
|
||||
"""权限检查依赖函数"""
|
||||
return PermissionChecker(permission)
|
||||
Reference in New Issue
Block a user