bmc_hub/app/modules/hardware/frontend/views.py
Christian 297a8ef2d6 feat: Implement ESET integration for hardware management
- Added ESET sync functionality to periodically fetch devices and incidents.
- Created new ESET service for API interactions, including authentication and data retrieval.
- Introduced new database tables for storing ESET incidents and hardware contacts.
- Updated hardware assets schema to include ESET-specific fields (UUID, specs, group).
- Developed frontend templates for ESET overview, import, and testing.
- Enhanced existing hardware creation form to auto-generate AnyDesk links.
- Added global logout functionality to clear user session data.
- Improved error handling and logging for ESET API interactions.
2026-02-11 13:23:32 +01:00

337 lines
12 KiB
Python

import logging
from fastapi import APIRouter, HTTPException, Query, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from datetime import date
from app.core.database import execute_query
logger = logging.getLogger(__name__)
router = APIRouter()
# Setup template directory - must be root "app" to allow extending shared/frontend/base.html
templates = Jinja2Templates(directory="app")
def build_location_tree(items: list) -> list:
"""Helper to build recursive location tree"""
nodes = {}
roots = []
for loc in items or []:
if not isinstance(loc, dict):
continue
loc_id = loc.get("id")
if loc_id is None:
continue
# Ensure we have the fields we need
nodes[loc_id] = {
"id": loc_id,
"name": loc.get("name"),
"location_type": loc.get("location_type"),
"parent_location_id": loc.get("parent_location_id"),
"children": []
}
for node in nodes.values():
parent_id = node.get("parent_location_id")
if parent_id and parent_id in nodes:
nodes[parent_id]["children"].append(node)
else:
roots.append(node)
def sort_nodes(node_list: list) -> None:
node_list.sort(key=lambda n: (n.get("name") or "").lower())
for n in node_list:
if n.get("children"):
sort_nodes(n["children"])
sort_nodes(roots)
return roots
@router.get("/hardware", response_class=HTMLResponse)
async def hardware_list(
request: Request,
status: str = Query(None),
asset_type: str = Query(None),
customer_id: int = Query(None),
q: str = Query(None)
):
"""Display list of all hardware."""
query = "SELECT * FROM hardware_assets WHERE deleted_at IS NULL"
params = []
if status:
query += " AND status = %s"
params.append(status)
if asset_type:
query += " AND asset_type = %s"
params.append(asset_type)
if customer_id:
query += " AND current_owner_customer_id = %s"
params.append(customer_id)
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"
hardware = execute_query(query, tuple(params))
# Get customer names for display
if hardware:
customer_ids = [h['current_owner_customer_id'] for h in hardware if h.get('current_owner_customer_id')]
if customer_ids:
customer_query = "SELECT id, name AS navn FROM customers WHERE id = ANY(%s)"
customers = execute_query(customer_query, (customer_ids,))
customer_map = {c['id']: c['navn'] for c in customers} if customers else {}
# Add customer names to hardware
for h in hardware:
if h.get('current_owner_customer_id'):
h['customer_name'] = customer_map.get(h['current_owner_customer_id'], 'Unknown')
return templates.TemplateResponse("modules/hardware/templates/index.html", {
"request": request,
"hardware": hardware,
"current_status": status,
"current_asset_type": asset_type,
"search_query": q
})
@router.get("/hardware/new", response_class=HTMLResponse)
async def create_hardware_form(request: Request):
"""Display create hardware form."""
# Get customers for dropdown
customers = execute_query("SELECT id, name AS navn FROM customers WHERE deleted_at IS NULL ORDER BY name")
return templates.TemplateResponse("modules/hardware/templates/create.html", {
"request": request,
"customers": customers or []
})
@router.get("/hardware/eset", response_class=HTMLResponse)
async def hardware_eset_overview(request: Request):
"""Display ESET sync overview (matches + incidents)."""
matches_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 500
"""
matches = execute_query(matches_query)
incidents_query = """
SELECT *
FROM eset_incidents
WHERE LOWER(COALESCE(severity, '')) IN ('critical', 'high', 'severe')
ORDER BY updated_at DESC NULLS LAST
LIMIT 200
"""
incidents = execute_query(incidents_query)
return templates.TemplateResponse("modules/hardware/templates/eset_overview.html", {
"request": request,
"matches": matches or [],
"incidents": incidents or []
})
@router.get("/hardware/eset/test", response_class=HTMLResponse)
async def hardware_eset_test(request: Request):
"""Display ESET API test page."""
return templates.TemplateResponse("modules/hardware/templates/eset_test.html", {
"request": request
})
@router.get("/hardware/eset/import", response_class=HTMLResponse)
async def hardware_eset_import(request: Request):
"""Display ESET import page."""
return templates.TemplateResponse("modules/hardware/templates/eset_import.html", {
"request": request
})
@router.get("/hardware/{hardware_id}", response_class=HTMLResponse)
async def hardware_detail(request: Request, hardware_id: int):
"""Display hardware details."""
# Get hardware
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")
hardware = result[0]
# Get customer name if applicable
if hardware.get('current_owner_customer_id'):
customer_query = "SELECT name AS navn FROM customers WHERE id = %s"
customer_result = execute_query(customer_query, (hardware['current_owner_customer_id'],))
if customer_result:
hardware['customer_name'] = customer_result[0]['navn']
# Get ownership history
ownership_query = """
SELECT * FROM hardware_ownership_history
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY start_date DESC
"""
ownership = execute_query(ownership_query, (hardware_id,))
# Get customer names for ownership history
if ownership:
customer_ids = [o['owner_customer_id'] for o in ownership if o.get('owner_customer_id')]
if customer_ids:
customer_query = "SELECT id, name AS navn FROM customers WHERE id = ANY(%s)"
customers = execute_query(customer_query, (customer_ids,))
customer_map = {c['id']: c['navn'] for c in customers} if customers else {}
for o in ownership:
if o.get('owner_customer_id'):
o['customer_name'] = customer_map.get(o['owner_customer_id'], 'Unknown')
# Get location history
location_query = """
SELECT * FROM hardware_location_history
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY start_date DESC
"""
locations = execute_query(location_query, (hardware_id,))
# Get attachments
attachment_query = """
SELECT * FROM hardware_attachments
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY uploaded_at DESC
"""
attachments = execute_query(attachment_query, (hardware_id,))
# Get related cases
case_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
"""
cases = execute_query(case_query, (hardware_id,))
# Get tags
tag_query = """
SELECT * FROM hardware_tags
WHERE hardware_id = %s AND deleted_at IS NULL
ORDER BY created_at DESC
"""
tags = execute_query(tag_query, (hardware_id,))
# Get all active locations for the tree (including parent_id for structure)
all_locations_query = """
SELECT id, name, location_type, parent_location_id
FROM locations_locations
WHERE deleted_at IS NULL
ORDER BY name
"""
all_locations_flat = execute_query(all_locations_query)
location_tree = build_location_tree(all_locations_flat)
return templates.TemplateResponse("modules/hardware/templates/detail.html", {
"request": request,
"hardware": hardware,
"ownership": ownership or [],
"locations": locations or [],
"attachments": attachments or [],
"cases": cases or [],
"tags": tags or [],
"location_tree": location_tree or []
})
@router.get("/hardware/{hardware_id}/edit", response_class=HTMLResponse)
async def edit_hardware_form(request: Request, hardware_id: int):
"""Display edit hardware form."""
# Get hardware
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")
hardware = result[0]
# Get customers for dropdown
customers = execute_query("SELECT id, name AS navn FROM customers WHERE deleted_at IS NULL ORDER BY name")
return templates.TemplateResponse("modules/hardware/templates/edit.html", {
"request": request,
"hardware": hardware,
"customers": customers or []
})
@router.post("/hardware/{hardware_id}/location")
async def update_hardware_location(
request: Request,
hardware_id: int,
location_id: int = Form(...),
notes: str = Form(None)
):
"""Update hardware location."""
# Verify hardware exists
check_query = "SELECT id FROM hardware_assets WHERE id = %s AND deleted_at IS NULL"
if not execute_query(check_query, (hardware_id,)):
raise HTTPException(status_code=404, detail="Hardware not found")
# Verify location exists
loc_check = "SELECT name FROM locations_locations WHERE id = %s"
loc_result = execute_query(loc_check, (location_id,))
if not loc_result:
raise HTTPException(status_code=404, detail="Location not found")
location_name = loc_result[0]['name']
# 1. Close current location history
close_history_query = """
UPDATE hardware_location_history
SET end_date = %s
WHERE hardware_id = %s AND end_date IS NULL
"""
execute_query(close_history_query, (date.today(), hardware_id))
# 2. Add new location history
add_history_query = """
INSERT INTO hardware_location_history (hardware_id, location_id, location_name, start_date, notes)
VALUES (%s, %s, %s, %s, %s)
"""
execute_query(add_history_query, (hardware_id, location_id, location_name, date.today(), notes))
# 3. Update current location on asset
update_asset_query = """
UPDATE hardware_assets
SET current_location_id = %s, updated_at = NOW()
WHERE id = %s
"""
execute_query(update_asset_query, (location_id, hardware_id))
return RedirectResponse(url=f"/hardware/{hardware_id}", status_code=303)