From 4a765dc8ee7941fda4fa5e69cbbf94c9cc155f61 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Fri, 6 Feb 2026 23:28:58 +0100 Subject: [PATCH] Added error management with JSON --- backend/db/hosts.py | 167 ++++++++++++++---- backend/main.py | 9 +- backend/routes/backup.py | 53 +++--- backend/routes/dhcp.py | 84 +++++---- backend/routes/dns.py | 37 ++-- backend/routes/hosts.py | 276 +++++++++++++++++++++++------ backend/routes/login.py | 9 +- frontend/js/hosts.js | 366 ++++++++++++++++++++++++++++----------- 8 files changed, 731 insertions(+), 270 deletions(-) diff --git a/backend/db/hosts.py b/backend/db/hosts.py index df68898..4901ccd 100644 --- a/backend/db/hosts.py +++ b/backend/db/hosts.py @@ -4,6 +4,8 @@ import ipaddress import logging import os +import re +import sqlite3 # Import local modules from backend.db.db import get_db, register_init # Import Settings @@ -11,6 +13,65 @@ from settings.settings import settings # Import Log from log.log import get_logger +# Regex for MAC check +MAC_RE = re.compile(r"^([0-9A-Fa-f]{2}([:\-])){5}([0-9A-Fa-f]{2})$") + +# ----------------------------- +# Check Data Input +# ----------------------------- +def validate_data(data: dict) -> dict: + if "name" not in data: + raise ValueError("Missing required field: name") + + name = str(data["name"]).strip() + if not name: + raise ValueError("Field 'name' cannot be empty") + + ipv4 = data.get("ipv4") + if ipv4: + try: + ipaddress.IPv4Address(ipv4) + except ValueError: + raise ValueError(f"Invalid IPv4 address: {ipv4}") + + ipv6 = data.get("ipv6") + if ipv6: + try: + ipaddress.IPv6Address(ipv6) + except ValueError: + raise ValueError(f"Invalid IPv6 address: {ipv6}") + + mac = data.get("mac") + if mac and not MAC_RE.match(mac): + raise ValueError(f"Invalid MAC address: {mac}") + + note = data.get("note") + + # Normalizzazione boolean per DB (0/1) + ssl_enabled = int(bool(data.get("ssl_enabled", 0))) + + return { + "name": name, + "ipv4": ipv4, + "ipv6": ipv6, + "mac": mac, + "note": note, + "ssl_enabled": ssl_enabled, + } + +# ----------------------------- +# Sorting hosts +# ----------------------------- +def ipv4_sort_key(h: dict): + v = (h.get('ipv4') or '').strip() + if not v: + # no ip at the end + return (1, 0) + try: + return (0, int(ipaddress.IPv4Address(v))) + except ValueError: + return (0, float('inf')) + # ----------------------------- # SELECT ALL HOSTS # ----------------------------- @@ -18,8 +79,7 @@ def get_hosts(): conn = get_db() cur = conn.execute("SELECT * FROM hosts") rows = [dict(r) for r in cur.fetchall()] - # Sort by IPv4 - rows.sort(key=lambda h: ipaddress.IPv4Address(h['ipv4'])) + rows.sort(key=ipv4_sort_key) return rows # ----------------------------- @@ -32,53 +92,92 @@ def get_host(host_id: int): return dict(row) if row else None # ----------------------------- -# INSERT HOST +# ADD HOST # ----------------------------- def add_host(data: dict): + + # Validate input + cleaned = validate_data(data) + conn = get_db() - cur = conn.execute( - "INSERT INTO hosts (name, ipv4, ipv6, mac, note, ssl_enabled) VALUES (?, ?, ?, ?, ?, ?)", - ( - data["name"], - data.get("ipv4"), - data.get("ipv6"), - data.get("mac"), - data.get("note"), - data.get("ssl_enabled", 0) + try: + cur = conn.execute( + "INSERT INTO hosts (name, ipv4, ipv6, mac, note, ssl_enabled) VALUES (?, ?, ?, ?, ?, ?)", + ( + cleaned["name"], + cleaned["ipv4"], + cleaned["ipv6"], + cleaned["mac"], + cleaned["note"], + cleaned["ssl_enabled"], + ) ) - ) - conn.commit() - last_id = cur.lastrowid - return last_id + conn.commit() + return cur.lastrowid + + except sqlite3.IntegrityError as e: + conn.rollback() + return -1 + + except Exception as e: + conn.rollback() + raise # ----------------------------- # UPDATE HOST # ----------------------------- -def update_host(host_id: int, data: dict): +def update_host(host_id: int, data: dict) -> bool: + + # Validate input + cleaned = validate_data(data) + conn = get_db() - conn.execute( - "UPDATE hosts SET name=?, ipv4=?, ipv6=?, mac=?, note=?, ssl_enabled=? WHERE id=?", - ( - data["name"], - data.get("ipv4"), - data.get("ipv6"), - data.get("mac"), - data.get("note"), - data.get("ssl_enabled", 0), - host_id + try: + cur = conn.execute( + """ + UPDATE hosts + SET name=?, ipv4=?, ipv6=?, mac=?, note=?, ssl_enabled=? + WHERE id=? + """, + ( + cleaned["name"], + cleaned["ipv4"], + cleaned["ipv6"], + cleaned["mac"], + cleaned["note"], + cleaned["ssl_enabled"], + host_id, + ) ) - ) - conn.commit() - return True + conn.commit() + return cur.rowcount > 0 + + except Exception: + conn.rollback() + raise # ----------------------------- # DELETE HOST # ----------------------------- -def delete_host(host_id: int): +def delete_host(host_id: int) -> bool: + + # Validate input + if host_id is None: + raise ValueError("host_id cannot be None") + conn = get_db() - conn.execute("DELETE FROM hosts WHERE id = ?", (host_id,)) - conn.commit() - return True + try: + cur = conn.execute( + "DELETE FROM hosts WHERE id = ?", + (host_id,) + ) + conn.commit() + + return cur.rowcount > 0 + + except Exception: + conn.rollback() + raise # ----------------------------- # Initialize Hosts DB Table diff --git a/backend/main.py b/backend/main.py index eeee807..4ff6fde 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ # import standard modules from contextlib import asynccontextmanager -from fastapi import FastAPI, Request +from fastapi import FastAPI, Request, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, RedirectResponse, JSONResponse, Response import logging @@ -155,7 +155,12 @@ async def session_middleware(request: Request, call_next): if path.startswith("/api"): if not is_logged_in(request): logger.error("API access denied - not logged in") - return JSONResponse({"error": "Unauthorized"}, status_code=401) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "error": "Unauthorized" + }, + ) response = await call_next(request) # Sliding expiration apply_session(response, username=None, token=token) diff --git a/backend/routes/backup.py b/backend/routes/backup.py index d0e2922..20df6b9 100644 --- a/backend/routes/backup.py +++ b/backend/routes/backup.py @@ -1,7 +1,7 @@ # backend/routes/backup.py # import standard modules -from fastapi import APIRouter, Request, Response +from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import asyncio import json @@ -41,32 +41,27 @@ async def api_dns_reload(request: Request): for h in hosts: f.write(json.dumps(h, ensure_ascii=False) + "\n") - except Exception as err: - error = True - message = str(err).strip() - - if error: - code = "BACKUP_ERROR" - # default del messaggio se vuoto o None - if not message: - message = "BACKUP error" - status = "failure" - #http_status = 500 - else: - code = "BACKUP_OK" - message = "BACKUP executed successfully" - status = "success" - #http_status = 200 + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "BACKUP_OK", + "status": "success", + "message": "BACKUP executed successfully", + "took_ms": took_ms, + }, + ) - took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - - payload = { - "code": code, - "status": status, - "message": message, - "details": { - "took_ms": took_ms - } - } - return JSONResponse(content=payload) - #return JSONResponse(content=payload, status_code=http_status) + except Exception as err: + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + logger = get_logger("hosts") + logger.exception("Error executing backup: %s", str(e).strip()) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "BACKUP_ERROR", + "status": "failure", + "message": "Internal error executing backup", + "took_ms": took_ms, + }, + ) diff --git a/backend/routes/dhcp.py b/backend/routes/dhcp.py index c1712bb..9ad16fe 100644 --- a/backend/routes/dhcp.py +++ b/backend/routes/dhcp.py @@ -1,7 +1,7 @@ # backend/routes/dhcp.py # import standard modules -from fastapi import APIRouter, Request, Response +from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import asyncio import csv @@ -69,25 +69,29 @@ async def api_dhcp_reload(request: Request): # RELOAD DHCP took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - payload = { - "code": "DHCP_RELOAD_OK", - "status": "success", - "message": "DHCP configuration reload successfully", - "details": {"took_ms": took_ms} - } - return JSONResponse(content=payload, status_code=200) + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "DHCP_RELOAD_OK", + "status": "success", + "message": "DHCP configuration reload successfully", + "took_ms": took_ms, + }, + ) except Exception as err: - get_logger("dhcp").exception("Error reloading DHCP: %s", str(err).strip()) + logger = get_logger("dhcp") + logger.exception("Error reloading DHCP: %s", str(err).strip()) took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - - payload = { - "code": "DHCP_RELOAD_ERROR", - "status": "failure", - "message": "Error reloading DHCP", - "details": {"took_ms": took_ms, "error": str(err).strip()} - } - return JSONResponse(content=payload, status_code=500) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "DHCP_RELOAD_ERROR", + "status": "failure", + "message": "Internal error reloading DHCP", + "took_ms": took_ms, + }, + ) # --------------------------------------------------------- # Get Leases @@ -101,12 +105,13 @@ def api_dhcp_leases(request: Request): try: path = Path(settings.DHCP4_LEASES_FILE) if not path.exists(): - return JSONResponse( - content={"code": "DHCP_LEASES_NOT_FOUND", - "status": "failure", - "message": f"DHCP leases file not found: {str(path)}", - "details": {}}, - status_code=404 + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "code": "DHCP_LEASES_NOT_FOUND", + "status": "failure", + "message": "File not found: " + str(path), + }, ) def _to_int(v: str): @@ -143,7 +148,12 @@ def api_dhcp_leases(request: Request): with path.open("r", encoding="utf-8", newline="") as f: reader = csv.DictReader(f) if not reader.fieldnames: - return JSONResponse(content={"total": 0, "items": []}, status_code=200) + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "total": 0, "items": [] + }, + ) for raw in reader: rec = { _norm(k): (v if v is not None else "") for k, v in raw.items() } @@ -164,14 +174,22 @@ def api_dhcp_leases(request: Request): } items.append(item) - return JSONResponse(content={"total": len(items), "items": items}, status_code=200) + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "total": len(items), + "items": items + }, + ) except Exception as err: - get_logger("dhcp").exception("Error reading DHCP leases: %s", str(err).strip()) - payload = { - "code": "DHCP_LEASES_ERROR", - "status": "failure", - "message": "Error reading DHCP leases", - "details": {"error": str(err).strip()} - } - return JSONResponse(content=payload, status_code=500) + logger = get_logger("dhcp") + logger.exception("Error reading DHCP leases: %s", str(err).strip()) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "DHCP_LEASES_ERROR", + "status": "failure", + "message": "Internal error reading DHCP leases", + }, + ) diff --git a/backend/routes/dns.py b/backend/routes/dns.py index c1174b5..43c9b9c 100644 --- a/backend/routes/dns.py +++ b/backend/routes/dns.py @@ -1,7 +1,7 @@ # backend/routes/dns.py # import standard modules -from fastapi import APIRouter, Request, Response +from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import asyncio import json @@ -52,22 +52,27 @@ async def api_dns_reload(request: Request): # RELOAD DNS took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - payload = { - "code": "DNS_RELOAD_OK", - "status": "success", - "message": "DNS configuration reload successfully", - "details": {"took_ms": took_ms} - } - return JSONResponse(content=payload, status_code=200) + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "DNS_RELOAD_OK", + "status": "success", + "message": "DNS configuration reload successfully", + "took_ms": took_ms, + }, + ) except Exception as err: - get_logger("dns").exception("Error reloading DNS: %s", str(err).strip()) + logger = get_logger("dns") + logger.exception("Error reloading DNS: %s", str(err).strip()) took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 - payload = { - "code": "DNS_RELOAD_ERROR", - "status": "failure", - "message": "Error reloading DNS", - "details": {"took_ms": took_ms, "error": str(err).strip()} - } - return JSONResponse(content=payload, status_code=500) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "DNS_RELOAD_ERROR", + "status": "failure", + "message": "Internal error reloading DNS", + "took_ms": took_ms, + }, + ) diff --git a/backend/routes/hosts.py b/backend/routes/hosts.py index 3a68e70..0aa6f36 100644 --- a/backend/routes/hosts.py +++ b/backend/routes/hosts.py @@ -1,10 +1,11 @@ # backend/routes/hosts.py # import standard modules -from fastapi import APIRouter, Request, Response -from fastapi.responses import FileResponse, RedirectResponse -import os +from fastapi import APIRouter, Request, Response, HTTPException, status +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse import ipaddress +import time +import os # Import local modules from backend.db.hosts import ( get_hosts, @@ -15,6 +16,8 @@ from backend.db.hosts import ( ) # Import Settings from settings.settings import settings +# Import Logging +from log.log import setup_logging, get_logger # Create Router router = APIRouter() @@ -33,56 +36,231 @@ def css_hosts(): return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/hosts.js")) # --------------------------------------------------------- -# API ENDPOINTS +# Get Hosts # --------------------------------------------------------- -@router.get("/api/hosts") +@router.get("/api/hosts", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Hosts found"}, + 500: {"description": "Internal server error"}, +}) def api_get_hosts(request: Request): - return get_hosts() + try: + hosts = get_hosts() + return hosts or [] -@router.post("/api/hosts") -def api_add_host(request: Request, data: dict): - name = data.get("name", "").strip() - ipv4 = data.get("ipv4") - ipv6 = data.get("ipv6") - if not name: - return {"error": "Name is required"} - if ipv4: - try: - ipaddress.IPv4Address(ipv4) - except: - return {"error": "Invalid IPv4 format"} - if ipv6: - try: - ipaddress.IPv6Address(ipv6) - except: - return {"error": "Invalid IPv6 format"} - return {"id": add_host(data)} - -@router.get("/api/hosts/{host_id}") + except Exception as e: + logger = get_logger("hosts") + logger.exception("Error getting list host %s", str(e).strip()) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "HOSTS_GET_ERROR", + "status": "failure", + "message": "Internal error getting host", + }, + ) + +# --------------------------------------------------------- +# Get Host +# --------------------------------------------------------- +@router.get("/api/hosts/{host_id}", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Host found"}, + 404: {"description": "Host not found"}, + 500: {"description": "Internal server error"}, +}) def api_get_host(request: Request, host_id: int): - return get_host(host_id) or {} -@router.put("/api/hosts/{host_id}") + try: + host = get_host(host_id) + if not host: # None or empty dict + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "code": "HOST_NOT_FOUND", + "status": "failure", + "message": "Host not found", + "host_id": host_id, + }, + ) + return host + + except Exception as e: + logger = get_logger("hosts") + logger.exception("Error adding host %s: %s", host_id, str(e).strip()) + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "HOST_GET_ERROR", + "status": "failure", + "message": "Internal error getting host", + "took_ms": took_ms, + }, + ) + +# --------------------------------------------------------- +# Add Hosts +# --------------------------------------------------------- +@router.post("/api/hosts", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Host added"}, + 409: {"description": "Host already present"}, + 500: {"description": "Internal server error"}, +}) +def api_add_host(request: Request, data: dict): + + # Inizializzazioni + start_ns = time.monotonic_ns() + + try: + host_id = add_host(data) + if(host_id > 0): + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "HOST_ADDED", + "status": "success", + "message": "Host added successfully", + "host_id": host_id, + "took_ms": took_ms, + }, + ) + + # Already present + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "code": "HOST_ALREADY_PRESENT", + "status": "failure", + "message": "Host already present", + "took_ms": took_ms, + }, + ) + + except HTTPException as httpe: + raise httpe + + except Exception as e: + logger = get_logger("hosts") + logger.exception("Error adding host: %s", str(e).strip()) + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "HOST_ADD_ERROR", + "status": "failure", + "message": "Internal error adding host", + "took_ms": took_ms, + }, + ) + +# --------------------------------------------------------- +# Update Host +# --------------------------------------------------------- +@router.put("/api/hosts/{host_id}", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Host updated"}, + 404: {"description": "Host not found"}, + 500: {"description": "Internal server error"}, +}) def api_update_host(request: Request, data: dict, host_id: int): - name = data.get("name", "").strip() - ipv4 = data.get("ipv4") - ipv6 = data.get("ipv6") - if not name: - return {"error": "Name is required"} - if ipv4: - try: - ipaddress.IPv4Address(ipv4) - except: - return {"error": "Invalid IPv4 format"} - if ipv6: - try: - ipaddress.IPv6Address(ipv6) - except: - return {"error": "Invalid IPv6 format"} - update_host(host_id, data) - return {"status": "ok"} - -@router.delete("/api/hosts/{host_id}") + + # Inizializzazioni + start_ns = time.monotonic_ns() + + try: + updated = update_host(host_id, data) + if updated: + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "HOST_UPDATED", + "status": "success", + "message": "Host updated successfully", + "host_id": host_id, + "took_ms": took_ms, + }, + ) + + # Not Found + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "code": "HOST_NOT_FOUND", + "status": "failure", + "message": "Host not found", + "host_id": host_id, + "took_ms": took_ms, + }, + ) + + except Exception as e: + logger = get_logger("hosts") + logger.exception("Error updating host %s: %s", host_id, str(e).strip()) + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "HOST_UPDATE_ERROR", + "status": "failure", + "message": "Internal error updating host", + "host_id": host_id, + "took_ms": took_ms, + }, + ) + +# --------------------------------------------------------- +# Delete +# --------------------------------------------------------- +@router.delete("/api/hosts/{host_id}", status_code=status.HTTP_200_OK, responses={ + 200: {"description": "Host deleted"}, + 404: {"description": "Host not found"}, + 500: {"description": "Internal server error"}, +}) def api_delete_host(request: Request, host_id: int): - delete_host(host_id) - return {"status": "ok"} + + # Inizializzazioni + start_ns = time.monotonic_ns() + + try: + deleted = delete_host(host_id) + if deleted: + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + return JSONResponse( + status_code=status.HTTP_200_OK, + content={ + "code": "HOST_DELETED", + "status": "success", + "message": "Host deleted successfully", + "details": {"took_ms": took_ms, "host_id": host_id,}, + }, + ) + + # Not Found + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "code": "HOST_NOT_FOUND", + "status": "failure", + "message": "Host not found", + "host_id": host_id, + "took_ms": took_ms, + }, + ) + + except Exception as e: + logger = get_logger("hosts") + logger.exception("Error deleting host %s: %s", host_id, str(e).strip()) + took_ms = (time.monotonic_ns() - start_ns) / 1_000_000 + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "code": "HOST_DELETE_ERROR", + "status": "failure", + "message": "Internal error deleting host", + "host_id": host_id, + "took_ms": took_ms, + }, + ) diff --git a/backend/routes/login.py b/backend/routes/login.py index e752165..6addb18 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -1,7 +1,7 @@ # backend/routes/login.py # import standard modules -from fastapi import APIRouter, Request, Response, HTTPException +from fastapi import APIRouter, Request, Response, HTTPException, status from fastapi.responses import FileResponse, RedirectResponse import os import time @@ -23,7 +23,12 @@ def check_rate_limit(ip: str): attempts = [t for t in attempts if now - t < settings.LOGIN_WINDOW_SECONDS] if len(attempts) >= settings.LOGIN_MAX_ATTEMPTS: - raise HTTPException(status_code=429, detail="Too many login attempts") + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "error": "Too many login attempts" + }, + ) # registra nuovo tentativo attempts.append(now) diff --git a/frontend/js/hosts.js b/frontend/js/hosts.js index ea82aac..0964eb1 100644 --- a/frontend/js/hosts.js +++ b/frontend/js/hosts.js @@ -58,23 +58,37 @@ function isValidMAC(mac) { async function loadHosts() { let hosts = []; try { - const res = await fetch("/api/hosts"); - if (!res.ok) { - const err = new Error(`Error loading hosts: ${res.status} ${res.statusText}`); + // Fetch data + const res = await fetch(`/api/hosts`, { + headers: { Accept: 'application/json' }, + }); + + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); err.status = res.status; throw err; } - // check content-type to avoid parsing errors - const contentType = res.headers.get("content-type") || ""; - if (!contentType.includes("application/json")) { - throw new Error("Answer is not JSON"); + + // Check JSON + let data; + try { + data = await res.json(); + hosts = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + + } catch { + throw new Error('Invalid JSON payload'); } - // parse data - const data = await res.json(); - hosts = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); - // debug log - //console.log("Hosts:", hosts); + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error loading hosts`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; + } } catch (err) { console.error(err?.message || "Error loading hosts"); @@ -86,7 +100,7 @@ async function loadHosts() { const tbody = document.querySelector("#hosts-table tbody"); if (!tbody) { console.warn('Element "#hosts-table tbody" not found in DOM.'); - return; + return; } // Svuota la tabella @@ -96,7 +110,7 @@ async function loadHosts() { if (!hosts.length) { const trEmpty = document.createElement("tr"); const tdEmpty = document.createElement("td"); - tdEmpty.colSpan = 6; + tdEmpty.colSpan = 7; tdEmpty.textContent = "No hosts available."; tdEmpty.style.textAlign = "center"; trEmpty.appendChild(tdEmpty); @@ -242,26 +256,46 @@ async function editHost(id) { // Clear form first clearAddHostForm(); - try { - const res = await fetch(`/api/hosts/${id}`); - if (!res.ok) throw new Error(`Fetch failed for host ${id}: ${res.status}`); - - const host = await res.json(); + // Fetch host + const res = await fetch(`/api/hosts/${id}`, { + headers: { Accept: 'application/json' }, + }); - // Store the ID of the host being edited - editingHostId = id; + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`Fetch failed for host ${id}: ${res.statusText}`); + err.status = res.status; + throw err; + } - // Pre-fill the form fields - document.getElementById("hostName").value = host.name ?? ""; - document.getElementById("hostIPv4").value = host.ipv4 ?? ""; - document.getElementById("hostIPv6").value = host.ipv6 ?? ""; - document.getElementById("hostMAC").value = host.mac ?? ""; - document.getElementById("hostNote").value = host.note ?? ""; - document.getElementById("hostSSL").checked = !!host.ssl_enabled; + // Check JSON + let host; + try { + host = await res.json(); + } catch { + throw new Error(`Fetch failed for host ${id}: Invalid JSON payload`); + } - } catch(err) { + // Check JSON errors + if (!res.ok) { + const serverMsg = host?.detail?.message?.trim(); + const base = `Fetch failed for host ${id}`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; throw err; } + + // Store the ID of the host being edited + editingHostId = id; + + // Pre-fill the form fields + document.getElementById("hostName").value = host.name ?? ""; + document.getElementById("hostIPv4").value = host.ipv4 ?? ""; + document.getElementById("hostIPv6").value = host.ipv6 ?? ""; + document.getElementById("hostMAC").value = host.mac ?? ""; + document.getElementById("hostNote").value = host.note ?? ""; + document.getElementById("hostSSL").checked = !!host.ssl_enabled; } // ----------------------------- @@ -289,55 +323,92 @@ async function saveHost(hostData) { return false; } - try { - if (editingHostId !== null) { - // UPDATE EXISTING HOST - const res = await fetch(`/api/hosts/${editingHostId}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(hostData) - }); - if (res.ok) { - showToast("Host updated successfully"); - } else { - throw new Error(`Update failed: ${res.status}`); - } - } else { - // CREATE NEW HOST - const res = await fetch("/api/hosts", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(hostData) - }); - if (res.ok) { - showToast("Host added successfully", true); - } else { - throw new Error(`Create failed: ${res.status}`); - } + if (editingHostId !== null) { + // UPDATE EXISTING HOST + const res = await fetch(`/api/hosts/${editingHostId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(hostData) + }); + + // Success without JSON + if (res.status === 204) { + showToast('Host updated successfully', true); + return true; } - } catch (err) { - console.error("Error saving host:", err); - throw err; - } - return true; -} -// ----------------------------- -// DELETE HOST -// ----------------------------- -async function deleteHost(id) { - try { - const res = await fetch(`/api/hosts/${id}`, { method: "DELETE" }); + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + } catch { + throw new Error('Invalid JSON payload'); + } + + // Check JSON errors if (!res.ok) { - throw new Error("Delete failed"); + const serverMsg = data?.detail?.message?.trim(); + const base = `Error updating host`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; } - showToast("Host removed successfully"); - } catch (err) { - console.error("Error deleting host:", err); - throw err; + // Success + showToast(data?.message || 'Host updated successfully', true); + return true; + + } else { + // CREATE NEW HOST + const res = await fetch(`/api/hosts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(hostData) + }); + + // Success without JSON + if (res.status === 204) { + showToast('Host created successfully', true); + return true; + } + + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + } catch { + throw new Error('Invalid JSON payload'); + } + + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error adding host`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; + } + + // Success + showToast(data?.message || 'Host created successfully', true); + return true } - await loadHosts(); } // ----------------------------- @@ -366,6 +437,7 @@ async function closeAddHostModal() { async function handleAddHostSubmit(e) { // Prevent default form submission e.preventDefault(); + // Retrieve form data const hostData = { name: document.getElementById('hostName').value.trim(), @@ -382,11 +454,14 @@ async function handleAddHostSubmit(e) { // close modal and reload hosts closeAddHostModal(); await loadHosts(); + return true } + } catch (err) { - console.error("Error saving host:", err); - showToast("Error saving host", false); + console.error(err?.message || "Error saving host"); + showToast(err?.message || "Error saving host", false); } + return false; } @@ -396,6 +471,7 @@ async function handleAddHostSubmit(e) { async function handleDeleteHost(e, el) { // Prevent default action e.preventDefault(); + // Get host ID const id = Number(el.dataset.hostId); if (!Number.isFinite(id)) { @@ -406,11 +482,50 @@ async function handleDeleteHost(e, el) { // Execute delete try { - deleteHost(id); + // Fetch data + const res = await fetch(`/api/hosts/${id}`, { + method: 'DELETE', + headers: { 'Accept': 'application/json' }, + }); + + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + } catch { + throw new Error('Invalid JSON payload'); + } + + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error deleting host`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; + } + + // Success + showToast(data?.message || 'Host deleted successfully', true); + + // Reload hosts + await loadHosts(); + return true; + } catch (err) { - console.error("Error deleting host:", err); - showToast("Error deleting host", false); + console.error(err?.message || "Error deleting host"); + showToast(err?.message || "Error deleting host", false); } + + return false; } // ----------------------------- @@ -418,32 +533,53 @@ async function handleDeleteHost(e, el) { // ----------------------------- async function handleReloadDNS() { try { - const res = await fetch(`/api/dns/reload`, { method: "POST" }); - if (!res.ok) { - const err = new Error(`Error reloading DNS: ${res.status} ${res.statusText}`); - err.status = res.status; - throw err; + // Fetch data + const res = await fetch(`/api/dns/reload`, { + method: 'POST', + headers: { 'Accept': 'application/json' }, + }); + + // Success without JSON + if (res.status === 204) { + showToast('DNS reload successfully', true); + return true; } - // check content-type to avoid parsing errors + + // Check content-type to avoid parsing errors const contentType = res.headers.get("content-type") || ""; if (!contentType.includes("application/json")) { - throw new Error("Answer is not JSON"); + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + } catch { + throw new Error('Invalid JSON payload'); } - const data = await res.json(); - if(data.code !== "DNS_RELOAD_OK"){ - const err = new Error(`Error reloading DNS: ${data.message}`); - err.status = data.code; + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error reloading DNS`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; throw err; } - //console.info("DNS Reload:", data); - showToast(data.message, true); + // Success + showToast(data?.message || 'DNS reload successfully', true); + return true; } catch (err) { console.error(err?.message || "Error reloading DNS"); showToast(err?.message || "Error reloading DNS", false); } + + return false; } // ----------------------------- @@ -451,32 +587,53 @@ async function handleReloadDNS() { // ----------------------------- async function handleReloadDHCP() { try { - const res = await fetch(`/api/dhcp/reload`, { method: "POST" }); - if (!res.ok) { - const err = new Error(`Error reloading DHCP: ${res.status} ${res.statusText}`); - err.status = res.status; - throw err; + // Fetch data + const res = await fetch(`/api/dhcp/reload`, { + method: 'POST', + headers: { 'Accept': 'application/json' }, + }); + + // Success without JSON + if (res.status === 204) { + showToast('DHCP reload successfully', true); + return true; } - // check content-type to avoid parsing errors + + // Check content-type to avoid parsing errors const contentType = res.headers.get("content-type") || ""; if (!contentType.includes("application/json")) { - throw new Error("Answer is not JSON"); + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + } catch { + throw new Error('Error reloading DHCP: Invalid JSON payload'); } - const data = await res.json(); - if(data.code !== "DHCP_RELOAD_OK"){ - const err = new Error(`Error reloading DHCP: ${data.message}`); - err.status = data.code; + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error reloadin DHCP`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; throw err; } - //console.info("DHCP Reload:", data); - showToast(data.message, true); + // Success + showToast(data?.message || 'DHCP reload successfully', true); + return true; } catch (err) { console.error(err?.message || "Error reloading DHCP"); showToast(err?.message || "Error reloading DHCP", false); } + + return false; } // ----------------------------- @@ -861,8 +1018,7 @@ document.addEventListener("DOMContentLoaded", async () => { try { await editHost(id); } catch (err) { - console.error("Error loading host for edit:", err); - showToast("Error loading host for edit", false); + showToast(err?.message || "Error loading host for edit", false); // Close modal const closeOnShown = () => { closeAddHostModal(lastTriggerEl); -- 2.47.3