]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added error management with JSON
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 6 Feb 2026 22:28:58 +0000 (23:28 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 6 Feb 2026 22:28:58 +0000 (23:28 +0100)
backend/db/hosts.py
backend/main.py
backend/routes/backup.py
backend/routes/dhcp.py
backend/routes/dns.py
backend/routes/hosts.py
backend/routes/login.py
frontend/js/hosts.js

index df6889849e3e6d1308087a9f5831e43559615e55..4901ccd3dd41e080ae6dc65aa4e9abc5083d1758 100644 (file)
@@ -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
index eeee80734e8b1c2ab60743c7312f129663f62edc..4ff6fde51a32c4e506d33efef8f152840b2b8d9c 100644 (file)
@@ -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)
index d0e2922e51bec3e4b4ccf4d248ba637bab17b8be..20df6b96a6f64a0b9738d70e13c87059fec88b19 100644 (file)
@@ -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,
+            },
+        )
index c1712bbd4e5039e0383852cd29921568de488b39..9ad16fed804a977e2be1ea65e48b3088246e6844 100644 (file)
@@ -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",
+            },
+        )
index c1174b5beeeb56d18cd284be1efb9dd4218aecd5..43c9b9c5d119a9cdc22fe963ab6245df735192bf 100644 (file)
@@ -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,
+            },
+        )
index 3a68e70936b1f7f5f7b9af40cc96cc9b8eedf324..0aa6f3635ff48ff215b6ebdd62f7978d7ca28913 100644 (file)
@@ -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,
+            },
+        )
index e7521654571c81418e20a12900e50f2995931685..6addb18bedcfa4003496b738c7c8601f6e364b9b 100644 (file)
@@ -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)
index ea82aac34426cb72a61ae5b3929c88d24db53633..0964eb163fc69e16bd86f642e43dd03aa305b8f8 100644 (file)
@@ -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);