Files
ipam/backend/app/services/user_service.py
T
Your Name 1e1e5957e8 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 记录
2026-07-18 09:45:30 +08:00

421 lines
14 KiB
Python

from typing import Optional, List
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
from fastapi import HTTPException, status
from app.models.auth import User, UserRole, UserStatus, Permission, RolePermission, RefreshToken
from app.core.security import SecurityUtils
# 默认权限项 + 角色权限映射
DEFAULT_PERMISSIONS = [
# 网段
("network:view", "查看网段", "network"),
("network:create", "创建网段", "network"),
("network:edit", "编辑网段", "network"),
("network:delete", "删除网段", "network"),
# IP
("ip:view", "查看 IP", "ip"),
("ip:edit", "编辑 IP", "ip"),
# 扫描
("scan:run", "执行扫描", "scan"),
("scan:view", "查看扫描结果", "scan"),
# SNMP
("snmp:view", "查看 SNMP", "snmp"),
("snmp:edit", "配置 SNMP", "snmp"),
# 告警
("alert:view", "查看告警", "alert"),
("alert:ack", "确认告警", "alert"),
("alert:resolve", "解决告警", "alert"),
# 报告
("report:export", "导出报告", "report"),
# 用户管理
("user:view", "查看用户", "system"),
("user:create", "创建用户", "system"),
("user:edit", "编辑用户", "system"),
("user:delete", "删除用户", "system"),
# 审计
("audit:view", "查看审计日志", "system"),
]
# 角色 -> 权限代码集合
DEFAULT_ROLE_PERMISSIONS = {
UserRole.SUPER_ADMIN: {p[0] for p in DEFAULT_PERMISSIONS}, # 全部
UserRole.ADMIN: {
"network:view", "network:create", "network:edit", "network:delete",
"ip:view", "ip:edit",
"scan:run", "scan:view",
"snmp:view", "snmp:edit",
"alert:view", "alert:ack", "alert:resolve",
"report:export",
"user:view", "audit:view",
},
UserRole.OPERATOR: {
"network:view",
"ip:view", "ip:edit",
"scan:run", "scan:view",
"snmp:view",
"alert:view", "alert:ack",
"report:export",
},
UserRole.VIEWER: {
"network:view",
"ip:view",
"scan:view",
"snmp:view",
"alert:view",
},
}
class UserService:
"""用户服务"""
@staticmethod
def init_default_permissions(db: Session) -> int:
"""初始化默认权限项和角色权限映射(已存在则跳过)"""
created = 0
perm_map = {}
for code, name, category in DEFAULT_PERMISSIONS:
existing = db.query(Permission).filter(Permission.code == code).first()
if existing:
perm_map[code] = existing
continue
p = Permission(code=code, name=name, category=category)
db.add(p)
db.flush()
perm_map[code] = p
created += 1
# 角色 -> 权限关联
for role, codes in DEFAULT_ROLE_PERMISSIONS.items():
for code in codes:
if code not in perm_map:
continue
pid = perm_map[code].id
link_existing = db.query(RolePermission).filter(
RolePermission.role == role,
RolePermission.permission_id == pid
).first()
if link_existing:
continue
db.add(RolePermission(role=role, permission_id=pid))
db.commit()
return created
@staticmethod
def get_user_permissions(db: Session, user: User) -> set:
"""获取用户拥有的全部权限代码集合"""
if user.role == UserRole.SUPER_ADMIN:
return {p.code for p in db.query(Permission).all()}
# 其他角色查 role_permissions 关联表
rows = db.query(Permission.code).join(
RolePermission, RolePermission.permission_id == Permission.id
).filter(RolePermission.role == user.role).all()
return {r[0] for r in rows}
@staticmethod
def init_default_admin(db: Session):
"""初始化默认管理员账户"""
admin_exists = db.query(User).filter(User.username == "admin").first()
if not admin_exists:
admin = User(
username="admin",
email="admin@ipam.local",
real_name="系统管理员",
password_hash=SecurityUtils.hash_password("admin123"),
role=UserRole.SUPER_ADMIN,
status=UserStatus.ACTIVE
)
db.add(admin)
db.commit()
db.refresh(admin)
return admin
return None
@staticmethod
def authenticate(db: Session, username: str, password: str) -> Optional[User]:
"""用户认证"""
user = db.query(User).filter(User.username == username).first()
if not user:
return None
if not SecurityUtils.verify_password(password, user.password_hash):
# 记录登录失败
user.login_failed_count += 1
# 登录失败5次锁定账号
if user.login_failed_count >= 5:
user.status = UserStatus.LOCKED
db.commit()
return None
# 登录成功,重置失败计数
if user.login_failed_count > 0:
user.login_failed_count = 0
db.commit()
# 检查用户状态
if user.status != UserStatus.ACTIVE:
return None
return user
@staticmethod
def create_user(db: Session,
username: str,
password: str,
email: Optional[str] = None,
phone: Optional[str] = None,
real_name: Optional[str] = None,
role: UserRole = UserRole.VIEWER,
created_by: Optional[int] = None) -> User:
"""创建用户"""
# 检查用户名是否存在
existing = db.query(User).filter(User.username == username).first()
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="用户名已存在"
)
# 检查邮箱是否存在
if email:
existing_email = db.query(User).filter(User.email == email).first()
if existing_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="邮箱已被使用"
)
user = User(
username=username,
email=email,
phone=phone,
real_name=real_name,
password_hash=SecurityUtils.hash_password(password),
role=role,
status=UserStatus.ACTIVE,
created_by=created_by
)
db.add(user)
db.commit()
db.refresh(user)
return user
@staticmethod
def update_login_info(db: Session, user_id: int, ip_address: str = None):
"""更新登录信息"""
user = db.query(User).filter(User.id == user_id).first()
if user:
user.last_login_at = datetime.utcnow()
user.last_login_ip = ip_address
db.commit()
@staticmethod
def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
"""根据ID获取用户"""
return db.query(User).filter(User.id == user_id).first()
@staticmethod
def get_user_by_username(db: Session, username: str) -> Optional[User]:
"""根据用户名获取用户"""
return db.query(User).filter(User.username == username).first()
@staticmethod
def get_users(db: Session, skip: int = 0, limit: int = 50, status: UserStatus = None, role: UserRole = None) -> tuple[int, List[User]]:
"""获取用户列表"""
query = db.query(User)
if status:
query = query.filter(User.status == status)
if role:
query = query.filter(User.role == role)
total = query.count()
items = query.order_by(User.id.desc()).offset(skip).limit(limit).all()
return total, items
@staticmethod
def change_password(db: Session, user_id: int, old_password: str, new_password: str) -> bool:
"""修改密码"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return False
if not SecurityUtils.verify_password(old_password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="原密码不正确"
)
user.password_hash = SecurityUtils.hash_password(new_password)
db.commit()
return True
@staticmethod
def reset_password(db: Session, user_id: int, new_password: str) -> bool:
"""管理员重置密码"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return False
user.password_hash = SecurityUtils.hash_password(new_password)
db.commit()
return True
@staticmethod
def update_user_status(db: Session, user_id: int, status: UserStatus) -> Optional[User]:
"""更新用户状态"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return None
user.status = status
db.commit()
db.refresh(user)
return user
@staticmethod
def delete_user(db: Session, user_id: int) -> bool:
"""删除用户(软删除,实际标记为禁用)"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return False
user.status = UserStatus.INACTIVE
db.commit()
return True
@staticmethod
def unlock_user(db: Session, user_id: int) -> Optional[User]:
"""解锁被锁定的用户"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return None
if user.status == UserStatus.LOCKED:
user.status = UserStatus.ACTIVE
user.login_failed_count = 0
db.commit()
db.refresh(user)
return user
class TokenService:
"""令牌服务"""
@staticmethod
def create_tokens(db: Session, user: User, ip_address: str = None, user_agent: str = None) -> dict:
"""创建访问令牌和刷新令牌"""
access_token_expires = timedelta(minutes=120) # 访问令牌2小时
access_token = SecurityUtils.create_access_token(
data={
"sub": user.username,
"user_id": user.id,
"role": user.role.value
},
expires_delta=access_token_expires
)
refresh_token_expires = timedelta(days=7) # 刷新令牌7天
refresh_token_value = SecurityUtils.create_refresh_token(
data={
"sub": user.username,
"user_id": user.id
},
expires_delta=refresh_token_expires
)
# 保存刷新令牌
refresh_token = RefreshToken(
user_id=user.id,
token=refresh_token_value,
expires_at=datetime.utcnow() + refresh_token_expires,
ip_address=ip_address,
user_agent=user_agent
)
db.add(refresh_token)
db.commit()
return {
"access_token": access_token,
"refresh_token": refresh_token_value,
"token_type": "bearer",
"expires_in": int(access_token_expires.total_seconds())
}
@staticmethod
def refresh_access_token(db: Session, refresh_token: str) -> Optional[dict]:
"""使用刷新令牌获取新的访问令牌"""
try:
payload = SecurityUtils.verify_token(refresh_token)
username = payload.get("sub")
user_id = payload.get("user_id")
# 检查刷新令牌是否有效且未被撤销
token_record = db.query(RefreshToken).filter(
RefreshToken.token == refresh_token,
RefreshToken.user_id == user_id,
RefreshToken.revoked == False,
RefreshToken.expires_at > datetime.utcnow()
).first()
if not token_record:
return None
user = db.query(User).filter(User.id == user_id).first()
if not user or user.status != UserStatus.ACTIVE:
return None
# 创建新的访问令牌
access_token_expires = timedelta(minutes=120)
access_token = SecurityUtils.create_access_token(
data={
"sub": user.username,
"user_id": user.id,
"role": user.role.value
},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": int(access_token_expires.total_seconds())
}
except Exception:
return None
@staticmethod
def revoke_token(db: Session, refresh_token: str) -> bool:
"""撤销刷新令牌"""
token_record = db.query(RefreshToken).filter(RefreshToken.token == refresh_token).first()
if token_record:
token_record.revoked = True
token_record.revoked_at = datetime.utcnow()
db.commit()
return True
return False
@staticmethod
def revoke_all_user_tokens(db: Session, user_id: int) -> int:
"""撤销用户所有刷新令牌"""
tokens = db.query(RefreshToken).filter(
RefreshToken.user_id == user_id,
RefreshToken.revoked == False
).all()
for token in tokens:
token.revoked = True
token.revoked_at = datetime.utcnow()
db.commit()
return len(tokens)