feat(桌面与监控): 完善工作台导航和登录活动定位
- 优化桌面标签、上下文标题、前进后退、导航栏隐藏和原生菜单体验 - 补充登录会话 IP 采集、地理位置回退、管理端展示及数据库迁移 - 更新桌面发布检查、运维文档和前后端测试覆盖
This commit is contained in:
@@ -28,6 +28,7 @@ from app.models.permission_metric_snapshot import PermissionMetricSnapshot
|
||||
from app.models.security_access_log import SecurityAccessLog
|
||||
from app.models.user import User
|
||||
from app.services.geo_location_metadata import GeoLocationMetadata, resolve_geo_location_metadata
|
||||
from app.services.ip_geolocation_fallback import resolve_external_ip_locations
|
||||
from app.services.ip_location import IpLocation, resolve_ip_location
|
||||
from app.services.monitoring_retention import get_monitoring_retention_status
|
||||
from app.services.permission_log_writer import get_log_writer
|
||||
@@ -1604,12 +1605,15 @@ async def get_ip_locations(
|
||||
SecurityAccessLog.category,
|
||||
)
|
||||
)
|
||||
permission_rows = permission_result.all()
|
||||
security_rows = security_result.all()
|
||||
buckets: dict[tuple[str, str, str], dict] = {}
|
||||
all_ip_addresses: set[str] = set()
|
||||
all_user_ids: set[str] = set()
|
||||
located_ip_addresses: set[str] = set()
|
||||
private_ip_addresses: set[str] = set()
|
||||
unknown_ip_addresses: set[str] = set()
|
||||
external_fallback_ip_addresses: set[str] = set()
|
||||
total_count = 0
|
||||
allowed_count = 0
|
||||
denied_count = 0
|
||||
@@ -1619,6 +1623,28 @@ async def get_ip_locations(
|
||||
resolved_locations: dict[str, IpLocation] = {}
|
||||
resolved_metadata: dict[str, GeoLocationMetadata] = {}
|
||||
|
||||
source_ip_addresses = list(
|
||||
dict.fromkeys(
|
||||
[str(row[0]) for row in permission_rows if row[0]]
|
||||
+ [str(row[0]) for row in security_rows if row[0]]
|
||||
)
|
||||
)
|
||||
for ip_address in source_ip_addresses:
|
||||
ip_info = resolve_ip_location(ip_address)
|
||||
resolved_locations[ip_address] = ip_info
|
||||
resolved_metadata[ip_address] = resolve_geo_location_metadata(ip_info)
|
||||
missing_coordinate_ips = [
|
||||
ip_address
|
||||
for ip_address, metadata in resolved_metadata.items()
|
||||
if metadata.accuracy_level != "private"
|
||||
and (metadata.longitude is None or metadata.latitude is None)
|
||||
]
|
||||
external_locations = await resolve_external_ip_locations(missing_coordinate_ips)
|
||||
for ip_address, external_location in external_locations.items():
|
||||
resolved_locations[ip_address] = external_location.merge_ip_location(resolved_locations[ip_address])
|
||||
resolved_metadata[ip_address] = external_location.to_metadata()
|
||||
external_fallback_ip_addresses.add(ip_address)
|
||||
|
||||
def normalize_observed_at(value: datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
@@ -1725,7 +1751,7 @@ async def get_ip_locations(
|
||||
if user_id:
|
||||
all_user_ids.add(str(user_id))
|
||||
|
||||
for ip_address, user_id, allowed, event_count, first_seen_at, last_seen_at in permission_result.all():
|
||||
for ip_address, user_id, allowed, event_count, first_seen_at, last_seen_at in permission_rows:
|
||||
add_ip_location_row(
|
||||
ip_address,
|
||||
user_id,
|
||||
@@ -1736,7 +1762,7 @@ async def get_ip_locations(
|
||||
source_kind="permission",
|
||||
)
|
||||
|
||||
for client_ip, user_identifier, auth_status, allowed, severity, category, event_count, first_seen_at, last_seen_at in security_result.all():
|
||||
for client_ip, user_identifier, auth_status, allowed, severity, category, event_count, first_seen_at, last_seen_at in security_rows:
|
||||
user_id = None
|
||||
if auth_status == "AUTHENTICATED" and user_identifier:
|
||||
try:
|
||||
@@ -1837,10 +1863,11 @@ async def get_ip_locations(
|
||||
},
|
||||
"server_location": server_location.to_public_dict() if server_location else None,
|
||||
"data_quality": {
|
||||
"resolver": "ip2region",
|
||||
"resolver": "ip2region+ip2location.io" if external_fallback_ip_addresses else "ip2region",
|
||||
"located_ip_count": located_count,
|
||||
"private_ip_count": private_count,
|
||||
"unknown_ip_count": unknown_count,
|
||||
"external_fallback_ip_count": len(external_fallback_ip_addresses),
|
||||
"location_coverage_rate": round((located_count / public_ip_count) * 100, 1) if public_ip_count else 100.0,
|
||||
"returned_region_count": len(items),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.crud import user as user_crud
|
||||
from app.crud import member as member_crud
|
||||
from app.utils.pagination import paginate
|
||||
from app.schemas.user import LoginStatus, UserCreate, UserLoginActivityRead, UserRead, UserStatus, UserUpdate
|
||||
from app.services.user_login_sessions import get_login_summaries, list_login_activities
|
||||
from app.services.user_login_sessions import get_login_summaries, list_login_activities, login_activity_payload
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -53,13 +53,16 @@ async def list_users(
|
||||
@router.get("/{user_id}/login-activities", response_model=list[UserLoginActivityRead])
|
||||
async def read_user_login_activities(
|
||||
user_id: uuid.UUID,
|
||||
response: Response,
|
||||
limit: int = Query(default=30, ge=1, le=100),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
current_user=Depends(require_roles(["ADMIN"])),
|
||||
) -> list[UserLoginActivityRead]:
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
if not await user_crud.get_by_id(db, user_id):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在")
|
||||
return await list_login_activities(db, user_id=user_id, limit=limit)
|
||||
activities = await list_login_activities(db, user_id=user_id, limit=limit)
|
||||
return [login_activity_payload(activity) for activity in activities]
|
||||
|
||||
|
||||
@router.post("/", response_model=UserRead, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
Reference in New Issue
Block a user