bmc_hub/app/modules/hardware/backend/router.py

899 lines
32 KiB
Python
Raw Normal View History

import logging
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query, UploadFile, File
from app.core.database import execute_query
from app.services.eset_service import eset_service
from psycopg2.extras import Json
from datetime import datetime, date
import os
import uuid
logger = logging.getLogger(__name__)
router = APIRouter()
def _eset_extract_first_str(payload: dict, keys: List[str]) -> Optional[str]:
if payload is None:
return None
key_set = {k.lower() for k in keys}
stack = [payload]
while stack:
current = stack.pop()
if isinstance(current, dict):
for k, v in current.items():
if k.lower() in key_set and isinstance(v, str) and v.strip():
return v.strip()
if isinstance(v, (dict, list)):
stack.append(v)
elif isinstance(current, list):
for item in current:
if isinstance(item, (dict, list)):
stack.append(item)
return None
def _eset_extract_group_path(payload: dict) -> Optional[str]:
return _eset_extract_first_str(payload, ["parentGroup", "groupPath", "group", "path"])
def _eset_extract_group_name(payload: dict) -> Optional[str]:
group_path = _eset_extract_group_path(payload)
if group_path and "/" in group_path:
name = group_path.split("/")[-1].strip()
return name or None
return group_path
def _eset_extract_company(payload: dict) -> Optional[str]:
company = _eset_extract_first_str(payload, ["company", "organization", "tenant", "customer", "userCompany"])
if company:
return company
group_path = _eset_extract_group_path(payload)
if group_path and "/" in group_path:
return group_path.split("/")[-1].strip() or None
return None
def _eset_detect_asset_type(payload: dict) -> str:
device_type = _eset_extract_first_str(payload, ["deviceType", "type"])
if device_type:
val = device_type.lower()
if "server" in val:
return "server"
if "laptop" in val or "notebook" in val:
return "laptop"
return "pc"
def _match_customer_exact(name: str) -> Optional[int]:
if not name:
return None
result = execute_query("SELECT id FROM customers WHERE LOWER(name) = LOWER(%s)", (name,))
if len(result or []) == 1:
return result[0]["id"]
return None
def _get_contact_customer(contact_id: int) -> Optional[int]:
query = """
SELECT customer_id
FROM contact_companies
WHERE contact_id = %s
ORDER BY is_primary DESC, id ASC
LIMIT 1
"""
result = execute_query(query, (contact_id,))
if result:
return result[0]["customer_id"]
return None
def _upsert_hardware_contact(hardware_id: int, contact_id: int) -> None:
query = """
INSERT INTO hardware_contacts (hardware_id, contact_id, role, source)
VALUES (%s, %s, %s, %s)
ON CONFLICT (hardware_id, contact_id) DO NOTHING
"""
execute_query(query, (hardware_id, contact_id, "primary", "eset"))
# ============================================================================
# CRUD Endpoints for Hardware Assets
# ============================================================================
@router.get("/hardware", response_model=List[dict])
async def list_hardware(
customer_id: Optional[int] = None,
status: Optional[str] = None,
asset_type: Optional[str] = None,
q: Optional[str] = None
):
"""List all hardware with optional filters."""
query = "SELECT * FROM hardware_assets WHERE deleted_at IS NULL"
params = []
if customer_id:
query += " AND current_owner_customer_id = %s"
params.append(customer_id)
if status:
query += " AND status = %s"
params.append(status)
if asset_type:
query += " AND asset_type = %s"
params.append(asset_type)
if q:
query += " AND (serial_number ILIKE %s OR model ILIKE %s OR brand ILIKE %s)"
search_param = f"%{q}%"
params.extend([search_param, search_param, search_param])
query += " ORDER BY created_at DESC"
result = execute_query(query, tuple(params))
logger.info(f"✅ Listed {len(result) if result else 0} hardware assets")
return result or []
@router.get("/hardware/by-customer/{customer_id}", response_model=List[dict])
async def list_hardware_by_customer(customer_id: int):
"""List hardware assets owned by a customer."""
query = """
SELECT * FROM hardware_assets
WHERE deleted_at IS NULL AND current_owner_customer_id = %s
ORDER BY created_at DESC
"""
result = execute_query(query, (customer_id,))
return result or []
@router.get("/hardware/by-contact/{contact_id}", response_model=List[dict])
async def list_hardware_by_contact(contact_id: int):
"""List hardware assets linked directly to a contact."""
query = """
SELECT DISTINCT h.*
FROM hardware_assets h
JOIN hardware_contacts hc ON hc.hardware_id = h.id
WHERE hc.contact_id = %s AND h.deleted_at IS NULL
ORDER BY h.created_at DESC
"""
result = execute_query(query, (contact_id,))
return result or []
@router.post("/hardware", response_model=dict)
async def create_hardware(data: dict):
"""Create a new hardware asset."""
try:
query = """
INSERT INTO hardware_assets (
asset_type, brand, model, serial_number, customer_asset_id,
internal_asset_id, notes, current_owner_type, current_owner_customer_id,
status, status_reason, warranty_until, end_of_life,
anydesk_id, anydesk_link,
eset_uuid, hardware_specs, eset_group
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
specs = data.get("hardware_specs")
if specs:
specs = Json(specs)
params = (
data.get("asset_type"),
data.get("brand"),
data.get("model"),
data.get("serial_number"),
data.get("customer_asset_id"),
data.get("internal_asset_id"),
data.get("notes"),
data.get("current_owner_type", "bmc"),
data.get("current_owner_customer_id"),
data.get("status", "active"),
data.get("status_reason"),
data.get("warranty_until"),
data.get("end_of_life"),
data.get("anydesk_id"),
data.get("anydesk_link"),
data.get("eset_uuid"),
specs,
data.get("eset_group")
)
result = execute_query(query, params)
if not result:
raise HTTPException(status_code=500, detail="Failed to create hardware")
hardware = result[0]
logger.info(f"✅ Created hardware asset: {hardware['id']} - {hardware['brand']} {hardware['model']}")
# Create initial ownership record if owner specified
if data.get("current_owner_type"):
ownership_query = """
INSERT INTO hardware_ownership_history (
hardware_id, owner_type, owner_customer_id, start_date, notes
)
VALUES (%s, %s, %s, %s, %s)
"""
ownership_params = (
hardware['id'],
data.get("current_owner_type"),
data.get("current_owner_customer_id"),
date.today(),
"Initial ownership record"
)
execute_query(ownership_query, ownership_params)
return hardware
except Exception as e:
logger.error(f"❌ Failed to create hardware: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/hardware/quick", response_model=dict)
async def quick_create_hardware(data: dict):
"""Quick create hardware with minimal fields (name + AnyDesk info)."""
try:
name = (data.get("name") or "").strip()
customer_id = data.get("customer_id")
anydesk_id = (data.get("anydesk_id") or "").strip() or None
anydesk_link = (data.get("anydesk_link") or "").strip() or None
if not name:
raise HTTPException(status_code=400, detail="Name is required")
if not customer_id:
raise HTTPException(status_code=400, detail="Customer ID is required")
query = """
INSERT INTO hardware_assets (
asset_type, model, current_owner_type, current_owner_customer_id,
status, anydesk_id, anydesk_link, notes
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
params = (
"andet",
name,
"customer",
customer_id,
"active",
anydesk_id,
anydesk_link,
"Quick created from case/ticket flow",
)
result = execute_query(query, params)
if not result:
raise HTTPException(status_code=500, detail="Failed to create hardware")
return result[0]
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Failed to quick-create hardware: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/hardware/{hardware_id}", response_model=dict)
async def get_hardware(hardware_id: int):
"""Get hardware details by ID."""
query = "SELECT * FROM hardware_assets WHERE id = %s AND deleted_at IS NULL"
result = execute_query(query, (hardware_id,))
if not result:
raise HTTPException(status_code=404, detail="Hardware not found")
logger.info(f"✅ Retrieved hardware: {hardware_id}")
return result[0]
@router.patch("/hardware/{hardware_id}", response_model=dict)
async def update_hardware(hardware_id: int, data: dict):
"""Update hardware asset."""
try:
# Build dynamic update query
update_fields = []
params = []
allowed_fields = [
"asset_type", "brand", "model", "serial_number", "customer_asset_id",
"internal_asset_id", "notes", "current_owner_type", "current_owner_customer_id",
"status", "status_reason", "warranty_until", "end_of_life",
"follow_up_date", "follow_up_owner_user_id", "anydesk_id", "anydesk_link",
"eset_uuid", "hardware_specs", "eset_group"
]
for field in allowed_fields:
if field in data:
update_fields.append(f"{field} = %s")
val = data[field]
if field == "hardware_specs" and val:
val = Json(val)
params.append(val)
if not update_fields:
raise HTTPException(status_code=400, detail="No valid fields to update")
update_fields.append("updated_at = NOW()")
params.append(hardware_id)
query = f"""
UPDATE hardware_assets
SET {', '.join(update_fields)}
WHERE id = %s AND deleted_at IS NULL
RETURNING *
"""
result = execute_query(query, tuple(params))
if not result:
raise HTTPException(status_code=404, detail="Hardware not found")
logger.info(f"✅ Updated hardware: {hardware_id}")
return result[0]
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Failed to update hardware {hardware_id}: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/hardware/{hardware_id}")
async def delete_hardware(hardware_id: int):
"""Soft-delete hardware asset."""
query = """
UPDATE hardware_assets
SET deleted_at = NOW()
WHERE id = %s AND deleted_at IS NULL
RETURNING id
"""
result = execute_query(query, (hardware_id,))
if not result:
raise HTTPException(status_code=404, detail="Hardware not found")
logger.info(f"✅ Deleted hardware: {hardware_id}")
return {"message": "Hardware deleted successfully"}
# ============================================================================
# Ownership History Endpoints
# ============================================================================
@router.get("/hardware/{hardware_id}/ownership", response_model=List[dict])
async def get_ownership_history(hardware_id: int):
"""Get ownership history for hardware."""
query = """
SELECT * FROM hardware_ownership_history
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY start_date DESC
"""
result = execute_query(query, (hardware_id,))
logger.info(f"✅ Retrieved ownership history for hardware: {hardware_id}")
return result or []
@router.post("/hardware/{hardware_id}/ownership", response_model=dict)
async def add_ownership_record(hardware_id: int, data: dict):
"""Add ownership record (auto-closes previous active ownership)."""
try:
# Close any active ownership records
close_query = """
UPDATE hardware_ownership_history
SET end_date = %s
WHERE hardware_id = %s AND end_date IS NULL AND deleted_at IS NULL
"""
execute_query(close_query, (date.today(), hardware_id))
# Create new ownership record
insert_query = """
INSERT INTO hardware_ownership_history (
hardware_id, owner_type, owner_customer_id, start_date, notes
)
VALUES (%s, %s, %s, %s, %s)
RETURNING *
"""
params = (
hardware_id,
data.get("owner_type"),
data.get("owner_customer_id"),
data.get("start_date", date.today()),
data.get("notes")
)
result = execute_query(insert_query, params)
# Update current owner in hardware_assets
update_query = """
UPDATE hardware_assets
SET current_owner_type = %s, current_owner_customer_id = %s, updated_at = NOW()
WHERE id = %s
"""
execute_query(update_query, (data.get("owner_type"), data.get("owner_customer_id"), hardware_id))
logger.info(f"✅ Added ownership record for hardware: {hardware_id}")
return result[0]
except Exception as e:
logger.error(f"❌ Failed to add ownership record: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Location History Endpoints
# ============================================================================
@router.get("/hardware/{hardware_id}/locations", response_model=List[dict])
async def get_location_history(hardware_id: int):
"""Get location history for hardware."""
query = """
SELECT * FROM hardware_location_history
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY start_date DESC
"""
result = execute_query(query, (hardware_id,))
logger.info(f"✅ Retrieved location history for hardware: {hardware_id}")
return result or []
@router.post("/hardware/{hardware_id}/locations", response_model=dict)
async def add_location_record(hardware_id: int, data: dict):
"""Add location record (auto-closes previous active location)."""
try:
# Close any active location records
close_query = """
UPDATE hardware_location_history
SET end_date = %s
WHERE hardware_id = %s AND end_date IS NULL AND deleted_at IS NULL
"""
execute_query(close_query, (date.today(), hardware_id))
# Create new location record
insert_query = """
INSERT INTO hardware_location_history (
hardware_id, location_id, location_name, start_date, notes
)
VALUES (%s, %s, %s, %s, %s)
RETURNING *
"""
params = (
hardware_id,
data.get("location_id"),
data.get("location_name"),
data.get("start_date", date.today()),
data.get("notes")
)
result = execute_query(insert_query, params)
# Update current location in hardware_assets
update_query = """
UPDATE hardware_assets
SET current_location_id = %s, updated_at = NOW()
WHERE id = %s
"""
execute_query(update_query, (data.get("location_id"), hardware_id))
logger.info(f"✅ Added location record for hardware: {hardware_id}")
return result[0]
except Exception as e:
logger.error(f"❌ Failed to add location record: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# Attachment Endpoints
# ============================================================================
@router.get("/hardware/{hardware_id}/attachments", response_model=List[dict])
async def get_attachments(hardware_id: int):
"""Get all attachments for hardware."""
query = """
SELECT * FROM hardware_attachments
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY uploaded_at DESC
"""
result = execute_query(query, (hardware_id,))
logger.info(f"✅ Retrieved {len(result) if result else 0} attachments for hardware: {hardware_id}")
return result or []
@router.post("/hardware/{hardware_id}/attachments", response_model=dict)
async def upload_attachment(hardware_id: int, data: dict):
"""Upload attachment for hardware."""
try:
# Generate storage reference (in production, this would upload to cloud storage)
storage_ref = f"hardware/{hardware_id}/{uuid.uuid4()}_{data.get('file_name')}"
query = """
INSERT INTO hardware_attachments (
hardware_id, file_type, file_name, storage_ref,
file_size_bytes, mime_type, description, uploaded_by_user_id
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
params = (
hardware_id,
data.get("file_type", "other"),
data.get("file_name"),
storage_ref,
data.get("file_size_bytes"),
data.get("mime_type"),
data.get("description"),
data.get("uploaded_by_user_id")
)
result = execute_query(query, params)
logger.info(f"✅ Uploaded attachment for hardware: {hardware_id}")
return result[0]
except Exception as e:
logger.error(f"❌ Failed to upload attachment: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/hardware/{hardware_id}/attachments/{attachment_id}")
async def delete_attachment(hardware_id: int, attachment_id: int):
"""Soft-delete attachment."""
query = """
UPDATE hardware_attachments
SET deleted_at = NOW()
WHERE id = %s AND hardware_id = %s AND deleted_at IS NULL
RETURNING id
"""
result = execute_query(query, (attachment_id, hardware_id))
if not result:
raise HTTPException(status_code=404, detail="Attachment not found")
logger.info(f"✅ Deleted attachment {attachment_id} for hardware: {hardware_id}")
return {"message": "Attachment deleted successfully"}
# ============================================================================
# Case Relations Endpoints
# ============================================================================
@router.get("/hardware/{hardware_id}/cases", response_model=List[dict])
async def get_related_cases(hardware_id: int):
"""Get all cases related to this hardware."""
query = """
SELECT hcr.*, s.titel, s.status, s.customer_id
FROM hardware_case_relations hcr
LEFT JOIN sag_sager s ON hcr.case_id = s.id
WHERE hcr.hardware_id = %s AND hcr.deleted_at IS NULL AND s.deleted_at IS NULL
ORDER BY hcr.created_at DESC
"""
result = execute_query(query, (hardware_id,))
logger.info(f"✅ Retrieved {len(result) if result else 0} related cases for hardware: {hardware_id}")
return result or []
@router.post("/hardware/{hardware_id}/cases", response_model=dict)
async def link_case(hardware_id: int, data: dict):
"""Link hardware to a case."""
try:
query = """
INSERT INTO hardware_case_relations (
hardware_id, case_id, relation_type
)
VALUES (%s, %s, %s)
RETURNING *
"""
params = (
hardware_id,
data.get("case_id"),
data.get("relation_type", "related")
)
result = execute_query(query, params)
logger.info(f"✅ Linked hardware {hardware_id} to case {data.get('case_id')}")
return result[0]
except Exception as e:
logger.error(f"❌ Failed to link case: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/hardware/{hardware_id}/cases/{case_id}")
async def unlink_case(hardware_id: int, case_id: int):
"""Unlink hardware from a case."""
query = """
UPDATE hardware_case_relations
SET deleted_at = NOW()
WHERE hardware_id = %s AND case_id = %s AND deleted_at IS NULL
RETURNING id
"""
result = execute_query(query, (hardware_id, case_id))
if not result:
raise HTTPException(status_code=404, detail="Case relation not found")
logger.info(f"✅ Unlinked hardware {hardware_id} from case {case_id}")
return {"message": "Case unlinked successfully"}
# ============================================================================
# Tag Endpoints
# ============================================================================
@router.get("/hardware/{hardware_id}/tags", response_model=List[dict])
async def get_tags(hardware_id: int):
"""Get all tags for hardware."""
query = """
SELECT * FROM hardware_tags
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY created_at DESC
"""
result = execute_query(query, (hardware_id,))
logger.info(f"✅ Retrieved {len(result) if result else 0} tags for hardware: {hardware_id}")
return result or []
@router.post("/hardware/{hardware_id}/tags", response_model=dict)
async def add_tag(hardware_id: int, data: dict):
"""Add tag to hardware."""
try:
query = """
INSERT INTO hardware_tags (
hardware_id, tag_name, tag_type
)
VALUES (%s, %s, %s)
RETURNING *
"""
params = (
hardware_id,
data.get("tag_name"),
data.get("tag_type", "manual")
)
result = execute_query(query, params)
logger.info(f"✅ Added tag '{data.get('tag_name')}' to hardware: {hardware_id}")
return result[0]
except Exception as e:
logger.error(f"❌ Failed to add tag: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/hardware/{hardware_id}/tags/{tag_id}")
async def delete_tag(hardware_id: int, tag_id: int):
"""Delete tag from hardware."""
query = """
UPDATE hardware_tags
SET deleted_at = NOW()
WHERE id = %s AND hardware_id = %s AND deleted_at IS NULL
RETURNING id
"""
result = execute_query(query, (tag_id, hardware_id))
if not result:
raise HTTPException(status_code=404, detail="Tag not found")
logger.info(f"✅ Deleted tag {tag_id} from hardware: {hardware_id}")
return {"message": "Tag deleted successfully"}
# ============================================================================
# Search Endpoint
# ============================================================================
@router.get("/search/hardware", response_model=List[dict])
async def search_hardware(q: str = Query(..., min_length=1)):
"""Search hardware by serial number, model, or brand."""
query = """
SELECT * FROM hardware_assets
WHERE deleted_at IS NULL
AND (
serial_number ILIKE %s
OR model ILIKE %s
OR brand ILIKE %s
OR customer_asset_id ILIKE %s
OR internal_asset_id ILIKE %s
)
ORDER BY created_at DESC
LIMIT 50
"""
search_param = f"%{q}%"
result = execute_query(query, (search_param, search_param, search_param, search_param, search_param))
logger.info(f"✅ Search for '{q}' returned {len(result) if result else 0} results")
return result or []
@router.post("/hardware/{hardware_id}/sync-eset", response_model=dict)
async def sync_eset_data(hardware_id: int, eset_uuid: Optional[str] = Query(None)):
"""Sync hardware data from ESET."""
# Get current hardware
check_query = "SELECT * FROM hardware_assets WHERE id = %s AND deleted_at IS NULL"
result = execute_query(check_query, (hardware_id,))
if not result:
raise HTTPException(status_code=404, detail="Hardware not found")
current = result[0]
# Determine UUID
uuid_to_use = eset_uuid or current.get("eset_uuid")
if not uuid_to_use:
raise HTTPException(status_code=400, detail="No ESET UUID provided or found on asset. Please provide 'eset_uuid' query parameter.")
# Fetch from ESET
details = await eset_service.get_device_details(uuid_to_use)
if not details:
raise HTTPException(status_code=404, detail="Device not found in ESET")
# Update hardware asset
update_data = {
"eset_uuid": uuid_to_use,
"hardware_specs": details
}
# We can perform the update directly here or call update_hardware if available
return await update_hardware(hardware_id, update_data)
@router.get("/hardware/eset/test", response_model=dict)
async def test_eset_device(device_uuid: str = Query(..., min_length=1)):
"""Test ESET device lookup by UUID."""
details = await eset_service.get_device_details(device_uuid)
if not details:
raise HTTPException(status_code=404, detail="Device not found in ESET")
return details
@router.get("/hardware/eset/devices", response_model=dict)
async def list_eset_devices(
page_size: Optional[int] = Query(None, ge=1, le=1000),
page_token: Optional[str] = Query(None)
):
"""List devices directly from ESET Device Management."""
payload = await eset_service.list_devices(page_size=page_size, page_token=page_token)
if not payload:
raise HTTPException(status_code=404, detail="No devices returned from ESET")
return payload
@router.post("/hardware/eset/import", response_model=dict)
async def import_eset_device(data: dict):
"""Import ESET device into hardware assets and optionally link to contact."""
device_uuid = (data.get("device_uuid") or "").strip()
contact_id = data.get("contact_id")
if not device_uuid:
raise HTTPException(status_code=400, detail="device_uuid is required")
details = await eset_service.get_device_details(device_uuid)
if not details:
raise HTTPException(status_code=404, detail="Device not found in ESET")
serial = _eset_extract_first_str(details, ["serialNumber", "serial", "serial_number"])
model = _eset_extract_first_str(details, ["model", "deviceModel", "deviceName", "name"])
brand = _eset_extract_first_str(details, ["manufacturer", "brand", "vendor"])
group_path = _eset_extract_group_path(details)
group_name = _eset_extract_group_name(details)
company = _eset_extract_company(details)
if contact_id:
contact_check = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,))
if not contact_check:
raise HTTPException(status_code=404, detail="Contact not found")
customer_id = _get_contact_customer(contact_id) if contact_id else None
if not customer_id:
customer_id = _match_customer_exact(group_name or company)
owner_type = "customer" if customer_id else "bmc"
conditions = ["eset_uuid = %s"]
params = [device_uuid]
if serial:
conditions.append("serial_number = %s")
params.append(serial)
lookup_query = f"SELECT * FROM hardware_assets WHERE deleted_at IS NULL AND ({' OR '.join(conditions)})"
existing = execute_query(lookup_query, tuple(params))
if existing:
hardware_id = existing[0]["id"]
update_fields = ["eset_uuid = %s", "hardware_specs = %s", "updated_at = NOW()"]
update_params = [device_uuid, Json(details)]
if group_path:
update_fields.append("eset_group = %s")
update_params.append(group_path)
if not existing[0].get("serial_number") and serial:
update_fields.append("serial_number = %s")
update_params.append(serial)
if not existing[0].get("model") and model:
update_fields.append("model = %s")
update_params.append(model)
if not existing[0].get("brand") and brand:
update_fields.append("brand = %s")
update_params.append(brand)
if customer_id:
update_fields.append("current_owner_type = %s")
update_params.append("customer")
update_fields.append("current_owner_customer_id = %s")
update_params.append(customer_id)
update_params.append(hardware_id)
update_query = f"""
UPDATE hardware_assets
SET {', '.join(update_fields)}
WHERE id = %s
RETURNING *
"""
hardware = execute_query(update_query, tuple(update_params))
hardware = hardware[0] if hardware else None
else:
insert_query = """
INSERT INTO hardware_assets (
asset_type, brand, model, serial_number,
current_owner_type, current_owner_customer_id,
notes, eset_uuid, hardware_specs, eset_group
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
insert_params = (
_eset_detect_asset_type(details),
brand,
model,
serial,
owner_type,
customer_id,
"Imported from ESET",
device_uuid,
Json(details),
group_path
)
hardware = execute_query(insert_query, insert_params)
hardware = hardware[0] if hardware else None
if hardware and contact_id:
_upsert_hardware_contact(hardware["id"], contact_id)
return hardware or {}
@router.get("/hardware/eset/matches", response_model=List[dict])
async def list_eset_matches(limit: int = Query(500, ge=1, le=2000)):
"""List ESET-matched hardware with contact/customer info."""
query = """
SELECT
h.id,
h.asset_type,
h.brand,
h.model,
h.serial_number,
h.eset_uuid,
h.eset_group,
h.updated_at,
hc.contact_id,
c.first_name,
c.last_name,
c.user_company,
cc.customer_id,
cust.name AS customer_name
FROM hardware_assets h
LEFT JOIN hardware_contacts hc ON hc.hardware_id = h.id
LEFT JOIN contacts c ON c.id = hc.contact_id
LEFT JOIN contact_companies cc ON cc.contact_id = c.id
LEFT JOIN customers cust ON cust.id = cc.customer_id
WHERE h.deleted_at IS NULL
ORDER BY h.updated_at DESC NULLS LAST
LIMIT %s
"""
result = execute_query(query, (limit,))
return result or []
@router.get("/hardware/eset/incidents", response_model=List[dict])
async def list_eset_incidents(
severity: Optional[str] = Query("critical"),
limit: int = Query(200, ge=1, le=2000)
):
"""List cached ESET incidents by severity."""
severity_list = [s.strip().lower() for s in (severity or "").split(",") if s.strip()]
if not severity_list:
severity_list = ["critical"]
query = """
SELECT *
FROM eset_incidents
WHERE LOWER(COALESCE(severity, '')) = ANY(%s)
ORDER BY updated_at DESC NULLS LAST
LIMIT %s
"""
result = execute_query(query, (severity_list, limit))
return result or []