]> git.giorgioravera.it Git - network-manager.git/commitdiff
Updated log and settings module & created config table in db
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Thu, 12 Mar 2026 16:36:00 +0000 (17:36 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Thu, 12 Mar 2026 16:36:00 +0000 (17:36 +0100)
32 files changed:
Dockerfile
backend/app.py
backend/bootstrap.py
backend/db/aliases.py
backend/db/config.py [new file with mode: 0644]
backend/db/db.py
backend/db/hosts.py
backend/db/users.py
backend/log/__init__.py [new file with mode: 0644]
backend/log/log.py [new file with mode: 0644]
backend/main.py
backend/routes/__init__.py [new file with mode: 0644]
backend/routes/about.py
backend/routes/aliases.py
backend/routes/backup.py
backend/routes/dhcp.py
backend/routes/dns.py
backend/routes/health.py
backend/routes/hosts.py
backend/routes/login.py
backend/security.py
backend/server.py
backend/settings/__init__.py [new file with mode: 0644]
backend/settings/config.py [new file with mode: 0644]
backend/settings/default.py [new file with mode: 0644]
backend/settings/settings.py [new file with mode: 0644]
log/__init__.py [deleted file]
log/log.py [deleted file]
settings/__init__.py [deleted file]
settings/config.py [deleted file]
settings/default.py [deleted file]
settings/settings.py [deleted file]

index db0c2611d875bf1e8c47d05ba20fb32a21ed4b9a..c31cacb977f8694a69c23306ccd7006b3f152382 100644 (file)
@@ -15,8 +15,6 @@ RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
 # Copy full application
 COPY backend backend
 COPY frontend frontend
-COPY log log
-COPY settings settings
 
 # ---------- STAGE 2: Alpine Runtime ----------
 FROM python:3.12-alpine
index 41eef4782b2dcb78be8d2e577e62750a0b8aa446..99b4c4a8195d14919a7025719f2ad5ea99d51db0 100644 (file)
@@ -23,8 +23,8 @@ from backend.routes.dhcp import router as dhcp_router
 from backend.security import is_logged_in, apply_session
 
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index ca3261ed0a27c650fad8142f5aeb01639e39cc6e..4515acb7af35f9e682db0743a037b62da02768f4 100644 (file)
@@ -3,14 +3,17 @@
 # Import standard modules
 import logging
 import os
+
 # Import backend modules
 from backend.db.db import init_db
+import backend.db.config
 import backend.db.users
 import backend.db.hosts
 import backend.db.aliases
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import setup_logging, get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # ------------------------------------------------------------------------------
 # Welcome log
index 038a770174affcb63144d4a872804990802f124f..cd2950cc20c3833baba46a4ca73f6bf7131b4944 100644 (file)
@@ -6,11 +6,12 @@ import logging
 import os
 import re
 import sqlite3
+
 # Import local modules
 from backend.db.db import get_db, register_init
-# Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+
+# Import Logging
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
diff --git a/backend/db/config.py b/backend/db/config.py
new file mode 100644 (file)
index 0000000..add67e2
--- /dev/null
@@ -0,0 +1,42 @@
+# backend/db/config.py
+
+# Import standard modules
+import os
+import sqlite3
+
+# Import local modules
+from backend.db.db import get_db, register_init
+
+# Import Settings & Logging
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
+
+# Logger initialization
+logger = get_logger(__name__)
+
+# -----------------------------
+# Return a specific config value
+# -----------------------------
+def get_config(key):
+    conn = get_db()
+    cur = conn.execute("SELECT value FROM config WHERE key = ?", (key,))
+    row = cur.fetchone()
+    return row["value"] if row else None
+
+# -----------------------------
+# Initialize Config DB Table
+# -----------------------------
+@register_init
+def init_db_hosts_table(cur):
+
+    # SETTINGS TABLE
+    cur.execute("""
+        CREATE TABLE config (
+            key TEXT PRIMARY KEY,
+            value TEXT
+        );
+    """)
+    cur.execute("INSERT INTO config (key, value) VALUES (?, ?)", ("domain", settings.DOMAIN))
+    cur.execute("INSERT INTO config (key, value) VALUES (?, ?)", ("external_name", settings.EXTERNAL_NAME))
+
+    logger.info("CONFIG DB: Tables initialized successfully")
index 99257fe011ddf392a5e5dd8e44c89921143d97af..8697f1497f520989d95a4fa56ba9bb05b05bbaad 100644 (file)
@@ -3,9 +3,10 @@
 # Import standard modules
 import os
 import sqlite3
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index 92c861c1b31fccb6b0df3a75e5dc02a448c344e6..b44d84a0e8b4149e49558e010fe053552e8360f8 100644 (file)
@@ -6,11 +6,12 @@ import logging
 import os
 import re
 import sqlite3
+
 # Import local modules
 from backend.db.db import get_db, register_init
-# Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+
+# Import Logging
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
@@ -197,16 +198,6 @@ def delete_host(host_id: int) -> bool:
 @register_init
 def init_db_hosts_table(cur):
 
-    # SETTINGS TABLE
-    cur.execute("""
-        CREATE TABLE settings (
-            key TEXT PRIMARY KEY,
-            value TEXT
-        );
-    """)
-    cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("domain", settings.DOMAIN))
-    cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("external_name", settings.EXTERNAL_NAME))
-
     # HOSTS TABLE
     cur.execute("""
         CREATE TABLE hosts (
index ffc682545e613aa9775e3750c63e4f357a55464e..1a116b45e220edf652f8abeb1d376a072e1ae645 100644 (file)
@@ -5,11 +5,13 @@ import bcrypt
 import json
 import logging
 import os
+
 # Import local modules
 from backend.db.db import get_db, register_init
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
diff --git a/backend/log/__init__.py b/backend/log/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/backend/log/log.py b/backend/log/log.py
new file mode 100644 (file)
index 0000000..18f43ab
--- /dev/null
@@ -0,0 +1,164 @@
+# backend/log/log.py
+
+from __future__ import annotations
+
+import logging
+import logging.config
+import os
+from typing import Optional
+
+# Module-level flag to prevent re-initialization
+_INITIALIZED = False
+
+# ---------------------------------------------------------
+# Build a full dictConfig for logging
+# ---------------------------------------------------------
+def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None,) -> dict:
+    """
+    Returns a complete dictConfig for the logging system, including formatters,
+    console/file handlers, and specific loggers (uvicorn, fastapi, etc).
+    """
+    level = (level or "INFO").upper()
+
+    formatters = {
+        # Generic Formatter
+        "detailed": {
+            "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
+            "datefmt": "%Y-%m-%dT%H:%M:%S%z",
+        },
+        # Access log Formatter
+        "access": {
+            "()": "uvicorn.logging.AccessFormatter",
+            "fmt": '%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s',
+            "datefmt": "%Y-%m-%dT%H:%M:%S%z",
+        },
+    }
+
+    handlers = {
+        # Generic Console (root/uvicorn/uvicorn.error/fastapi/app)
+        "console": {
+            "class": "logging.StreamHandler",
+            "level": level,
+            "formatter": "detailed",
+            "stream": "ext://sys.stdout",
+        },
+        # Access log Console
+        "access_console": {
+            "class": "logging.StreamHandler",
+            "level": level,
+            "formatter": "access",
+            "stream": "ext://sys.stdout",
+        },
+    }
+
+    # Select active handler based on console
+    active_handlers = ["console"]
+    access_handlers = ["access_console"]
+
+    if to_file:
+        if log_file is not None:
+            # Ensure the log directory exists and add a rotating file handler
+            log_dir = os.path.dirname(log_file) or "."
+            os.makedirs(log_dir, exist_ok=True)
+            # handler for generic log
+            handlers["file"] = {
+                "class": "logging.handlers.RotatingFileHandler",
+                "level": level,
+                "formatter": "detailed",
+                "filename": log_file,
+                "maxBytes": 5 * 1024 * 1024,
+                "backupCount": 5,
+                "encoding": "utf-8",
+            }
+            # Add active handler for generic log file
+            active_handlers.append("file")
+        if log_access_file is not None:
+            # Ensure the log directory exists and add a rotating file handler
+            log_dir = os.path.dirname(log_access_file) or "."
+            os.makedirs(log_dir, exist_ok=True)
+            # handler for access log
+            handlers["access_file"] = {
+                "class": "logging.handlers.RotatingFileHandler",
+                "level": level,
+                "formatter": "access",
+                "filename": log_access_file,
+                "maxBytes": 5 * 1024 * 1024,
+                "backupCount": 5,
+                "encoding": "utf-8",
+            }
+            # Add active handler for access log file
+            access_handlers.append("access_file")
+
+    return {
+        "version": 1,
+        "disable_existing_loggers": False,
+        "formatters": formatters,
+        "handlers": handlers,
+
+        # Root logger
+        "root": {
+            "level": level,
+            "handlers": active_handlers,
+        },
+        # Modules
+        "loggers": {
+            # Uvicorn core
+            "uvicorn": {
+                "level": level,
+                "handlers": active_handlers,
+                "propagate": False,
+            },
+            # Uvicorn internal error
+            "uvicorn.error": {
+                "level": level,
+                "handlers": active_handlers,
+                "propagate": False,
+            },
+            # Uvicorn access log
+            "uvicorn.access": {
+                "level": level,
+                "handlers": access_handlers,
+                "propagate": False,
+            },
+            # FastAPI
+            "fastapi": {
+                "level": level,
+                "handlers": active_handlers,
+                "propagate": False,
+            },
+            # Logger applicativo di comodo (puoi usarlo come logging.getLogger("main"))
+            "main": {
+                "level": level,
+                "handlers": active_handlers,
+                "propagate": False,
+            },
+        },
+    }
+
+# ---------------------------------------------------------
+# Initialize logging once (singleton guard)
+# ---------------------------------------------------------
+def setup_logging(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None, *, force: bool = False) -> None:
+    """
+    Initializes the logging system only once. Subsequent calls are no-ops.
+    Useful to prevent duplicated handlers or reconfiguration side effects.
+    """
+    global _INITIALIZED
+
+    if _INITIALIZED and not force:
+        return
+
+    config = build_log_config(level=level, to_file=to_file, log_file=log_file, log_access_file=log_access_file)
+    logging.config.dictConfig(config)
+
+    _INITIALIZED = True
+
+# ---------------------------------------------------------
+# Get a configured logger for the given module/name
+# ---------------------------------------------------------
+def get_logger(name: str | None = None) -> logging.Logger:
+    """
+    Returns a logger instance configured via the module setup. If setup was not
+    called yet, it falls back to the standard logging defaults.
+    """
+    return logging.getLogger(name or "main")
index 67d407a6504238450c3f414ebafe1cfb4bd53cfb..21c58182824d825e9866495abfb3ce7afa42ff62 100644 (file)
@@ -2,6 +2,7 @@
 
 # Import standard modules
 import os
+
 # Import backend modules
 from backend.bootstrap import bootstrap
 from backend.app import create_app
diff --git a/backend/routes/__init__.py b/backend/routes/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index 2cdf606972ce4c91d5907561c66d6662f16bc0d8..1f7a1caad911d91c0976a42ca741c9bfa36fcbb9 100644 (file)
@@ -2,8 +2,12 @@
 
 # Import standard modules
 from fastapi import APIRouter
+
+# Import local modules
+from backend.db.config import get_config
+
 # Import Settings
-from settings.settings import settings
+from backend.settings.settings import settings
 
 # Create Router
 router = APIRouter()
@@ -13,10 +17,10 @@ router = APIRouter()
 # ---------------------------------------------------------
 @router.get("/about")
 def about():
+    domain = get_config("domain")
     return {
         "app": {
             "version": settings.APP_VERSION,
         },
-        "domain": settings.DOMAIN,
-        "admin_hash_loaded": settings.ADMIN_PASSWORD_HASH is not None,
+        "domain": domain,
     }
index cd0f686bbf95790aa1803ae33f9288435d117c0f..98bbd7d1c284483336c1dc2142fe7249d01dc9c6 100644 (file)
@@ -6,6 +6,7 @@ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
 import ipaddress
 import time
 import os
+
 # Import local modules
 from backend.db.aliases import (
     get_aliases,
@@ -14,9 +15,10 @@ from backend.db.aliases import (
     update_alias,
     delete_alias
 )
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index 31d86e9aeb006b17474867a3e5ce62567ac081a8..ae70764b0e2a9712ce40305c3cd62228cbb6578f 100644 (file)
@@ -8,11 +8,13 @@ import json
 import os
 import ipaddress
 import time
+
 # Import local modules
 from backend.db.hosts import get_hosts
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index 73fe0e823e7147dd37ce798a2dec8ed92ba486c0..72755a8d4708023e1e7666c26a26dd7d9f12da3d 100644 (file)
@@ -10,11 +10,13 @@ import os
 import ipaddress
 from pathlib import Path
 import time
+
 # Import local modules
 from backend.db.hosts import get_hosts
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index c26086501fcd46b2693701be4d1b601b9fe11b83..3b06526eccf99ab0232979b82210f49b281afc0d 100644 (file)
@@ -8,12 +8,15 @@ import json
 import os
 import ipaddress
 import time
+
 # Import local modules
+from backend.db.config import get_config
 from backend.db.hosts import get_hosts
 from backend.db.aliases import get_aliases
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
@@ -41,6 +44,9 @@ async def api_dns_reload(request: Request):
                 line = f"{h.get('name')}\t\t IN\tA\t{h.get('ipv4')}\n"
                 f.write(line)
 
+        # Get Domain
+        domain = get_config("domain")
+
         # Save DNS Reverse Configuration
         path = settings.DNS_REVERSE_FILE
         with open(path, "w", encoding="utf-8") as f:
@@ -49,11 +55,11 @@ async def api_dns_reload(request: Request):
                 if ip:
                     parts = ip.split(".")
                     rev = f"{parts[-1]}.{parts[-2]}"
-                    line = f"{rev}\t\t IN PTR\t{h.get('name')}.{settings.DOMAIN}\n"
+                    line = f"{rev}\t\t IN PTR\t{h.get('name')}.{domain}\n"
                     f.write(line)
 
         # Get Aliases List
-        hosts = get_aliases()
+        aliases = get_aliases()
 
         # Save DNS Aliases Configuration
         path = settings.DNS_ALIAS_FILE
index bf9cc3fb1fc28c3124df4d3d719f86a48723cb76..c3575203c2d28be64164001c4f3c8221e394ff4e 100644 (file)
@@ -5,9 +5,10 @@ from fastapi import APIRouter
 import sqlite3
 import time
 import os
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index bcb26d004fa9afc2ce29cc5a5d18b569316775bc..48648305147fddefe4f28a1a9531cdd33f821fc2 100644 (file)
@@ -6,6 +6,7 @@ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
 import ipaddress
 import time
 import os
+
 # Import local modules
 from backend.db.hosts import (
     get_hosts,
@@ -14,9 +15,10 @@ from backend.db.hosts import (
     update_host,
     delete_host
 )
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index 42ff197e567197b4dcb64544698dd35d104df137..608ba8564b811561bcd636c8bb7fe586ef98645f 100644 (file)
@@ -5,10 +5,12 @@ from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse, RedirectResponse
 import os
 import time
+
 # Import local modules
 from backend.security import verify_login, apply_session, close_session
-# Import Settings
-from settings.settings import settings
+
+# Import Settings & Logging
+from backend.settings.settings import settings
 
 # Create Router
 router = APIRouter()
index 69b49ced9736826fad43e6ef4147395f763e9d7d..99f8db63d5f0e985bdba81356dda1f853b36eb48 100644 (file)
@@ -5,11 +5,13 @@ import bcrypt
 import os
 from fastapi import Request, HTTPException
 from itsdangerous import TimestampSigner
+
 # Import local modules
 from backend.db.users import get_user_by_username
+
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
index 3b732819d3d71ac9ff78f87ba6eac59b8d2c4a54..d69eaf33b37a0a2f3a7f1503abff163366a531bc 100644 (file)
@@ -4,8 +4,8 @@
 import uvicorn
 
 # Import Settings & Logging
-from settings.settings import settings
-from log.log import get_logger
+from backend.settings.settings import settings
+from backend.log.log import setup_logging, get_logger
 
 # Logger initialization
 logger = get_logger(__name__)
diff --git a/backend/settings/__init__.py b/backend/settings/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/backend/settings/config.py b/backend/settings/config.py
new file mode 100644 (file)
index 0000000..e5aea1a
--- /dev/null
@@ -0,0 +1,7 @@
+# backend/settings/config.py
+
+# ---------------------------------------------------------
+# APP
+# ---------------------------------------------------------
+APP_NAME = "network-manager"
+APP_VERSION = "0.0.2"
diff --git a/backend/settings/default.py b/backend/settings/default.py
new file mode 100644 (file)
index 0000000..906e1a2
--- /dev/null
@@ -0,0 +1,61 @@
+# backend/settings/default.py
+
+# ---------------------------------------------------------
+# Frontend
+# ---------------------------------------------------------
+FRONTEND_DIR = "/app/frontend"
+
+# ---------------------------------------------------------
+# Data Path (DB + Backup)
+# ---------------------------------------------------------
+DATA_PATH = "/data"
+
+# ---------------------------------------------------------
+# Database
+# ---------------------------------------------------------
+DB_FILE = "database.db"
+DB_RESET = False
+
+# ---------------------------------------------------------
+# Log
+# ---------------------------------------------------------
+LOG_LEVEL = "INFO"
+LOG_TO_FILE = False
+LOG_FILE = "app.log"
+LOG_ACCESS_FILE = "access.log"
+
+# ---------------------------------------------------------
+# Host
+# ---------------------------------------------------------
+DOMAIN = "example.com"
+EXTERNAL_NAME = "dyndns.example.com"
+
+# ---------------------------------------------------------
+# Web
+# ---------------------------------------------------------
+HTTP_HOST = "0.0.0.0"
+HTTP_PORT = "8000"
+LOGIN_MAX_ATTEMPTS = "5"
+LOGIN_WINDOW_SECONDS = "600"
+
+# ---------------------------------------------------------
+# Admin
+# ---------------------------------------------------------
+ADMIN_USER = "admin"
+ADMIN_PASSWORD = "admin"
+ADMIN_PASSWORD_HASH_FILE = "/run/secrets/admin_password_hash"
+
+# ---------------------------------------------------------
+# DNS
+# ---------------------------------------------------------
+DNS_HOST_FILE=f"/dns/etc/{DOMAIN}/hosts.inc"
+DNS_ALIAS_FILE=f"/dns/etc/{DOMAIN}/alias.inc"
+DNS_REVERSE_FILE="/dns/etc/reverse/hosts.inc"
+
+# ---------------------------------------------------------
+# DHCP
+# ---------------------------------------------------------
+DHCP4_HOST_FILE="/dhcp/etc/hosts-ipv4.json"
+DHCP4_LEASES_FILE="/dhcp/lib/dhcp4.leases"
+DHCP6_HOST_FILE="/dhcp/etc/hosts-ipv6.json"
+DHCP6_LEASES_FILE="/dhcp/lib/dhcp6.leases"
diff --git a/backend/settings/settings.py b/backend/settings/settings.py
new file mode 100644 (file)
index 0000000..aacb5da
--- /dev/null
@@ -0,0 +1,118 @@
+# backend/settings/settings.py
+
+from __future__ import annotations
+
+# import standard modules
+import os
+import secrets
+import datetime
+from pathlib import Path
+from typing import Optional
+from pydantic import BaseModel, Field, field_validator
+
+# Import Parameters
+from . import config, default
+
+# ---------------------------------------------------------
+# Convert value to boolean
+# ---------------------------------------------------------
+def _to_bool(val) -> bool:
+    if isinstance(val, bool):
+        return val
+    if val is None:
+        return False
+    return str(val).strip().lower() in {"1", "true", "yes", "on"}
+
+# ---------------------------------------------------------
+# Read text from file if it exists
+# ---------------------------------------------------------
+def _read_text_if_exists(path: Optional[str]) -> Optional[str]:
+    if not path:
+        return None
+    p = Path(path)
+    if p.exists() and p.is_file():
+        try:
+            return p.read_text(encoding="utf-8").strip()
+        except Exception:
+            return None
+    return None
+
+# ---------------------------------------------------------
+# Settings Model
+# ---------------------------------------------------------
+class Settings(BaseModel):
+    # Naming
+    APP_NAME: str = Field(default_factory=lambda: config.APP_NAME)
+
+    # Versioning
+    APP_VERSION: str = Field(default_factory=lambda: config.APP_VERSION)
+    DEVEL: bool = Field(default_factory=lambda: _to_bool(os.getenv("DEV", False)))
+
+    # DATA_PATH
+    DATA_PATH: str = Field(default_factory=lambda: os.getenv("DATA_PATH", default.DATA_PATH))
+
+    # Frontend
+    FRONTEND_DIR: str = Field(default_factory=lambda: os.getenv("FRONTEND_DIR", default.FRONTEND_DIR))
+
+    # Database
+    DB_FILE: str = Field(default_factory=lambda: os.getenv("DB_FILE", default.DB_FILE))
+    DB_RESET: bool = Field(default_factory=lambda: _to_bool(os.getenv("DB_RESET", default.DB_RESET)))
+
+    # Log
+    LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL))
+    LOG_TO_FILE: bool = Field(default_factory=lambda: _to_bool(os.getenv("LOG_TO_FILE", default.LOG_TO_FILE)))
+    LOG_FILE: str = Field(default_factory=lambda: os.getenv("LOG_FILE", default.LOG_FILE))
+    LOG_ACCESS_FILE: str = Field(default_factory=lambda: os.getenv("LOG_ACCESS_FILE", default.LOG_ACCESS_FILE))
+
+    # Hosts
+    DOMAIN: str = Field(default_factory=lambda: os.getenv("DOMAIN", default.DOMAIN))
+    EXTERNAL_NAME: str = Field(default_factory=lambda: os.getenv("EXTERNAL_NAME", default.DOMAIN))
+
+    # Web
+    HTTP_HOST: str = Field(default_factory=lambda: os.getenv("HTTP_HOST", default.HTTP_HOST))
+    HTTP_PORT: int = Field(default_factory=lambda: int(os.getenv("HTTP_PORT", default.HTTP_PORT)))
+    SECRET_KEY: str = Field(default_factory=lambda: (
+        (os.getenv("SESSION_SECRET") or _read_text_if_exists(os.getenv("SECRET_KEY_FILE")) or secrets.token_urlsafe(64)).strip()
+    ))
+    LOGIN_MAX_ATTEMPTS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_MAX_ATTEMPTS", default.LOGIN_MAX_ATTEMPTS)))
+    LOGIN_WINDOW_SECONDS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_WINDOW_SECONDS", default.LOGIN_WINDOW_SECONDS)))
+
+    # Admin
+    ADMIN_USER: str = Field(default_factory=lambda: os.getenv("ADMIN_USER", default.ADMIN_USER))
+    ADMIN_PASSWORD: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD", default.ADMIN_PASSWORD))
+    ADMIN_PASSWORD_HASH_FILE: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE))
+    ADMIN_PASSWORD_HASH: Optional[str] = Field(default_factory=lambda: (
+        (os.getenv("ADMIN_PASSWORD_HASH") or _read_text_if_exists(os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)) or None)
+    ))
+
+    # DNS
+    DNS_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DNS_HOST_FILE", default.DNS_HOST_FILE))
+    DNS_ALIAS_FILE: str = Field(default_factory=lambda: os.getenv("DNS_ALIAS_FILE", default.DNS_ALIAS_FILE))
+    DNS_REVERSE_FILE: str = Field(default_factory=lambda: os.getenv("DNS_REVERSE_FILE", default.DNS_REVERSE_FILE))
+    # DHCP
+    DHCP4_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_HOST_FILE", default.DHCP4_HOST_FILE))
+    DHCP4_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_LEASES_FILE", default.DHCP4_LEASES_FILE))
+    DHCP6_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_HOST_FILE", default.DHCP6_HOST_FILE))
+    DHCP6_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_LEASES_FILE", default.DHCP6_LEASES_FILE))
+
+    def model_post_init(self, __context) -> None:
+        if self.DEVEL:
+            ts = datetime.datetime.now().strftime("%Y%m%d-%H%M")
+            object.__setattr__(self, "APP_VERSION", f"{self.APP_VERSION}-dev-{ts}")
+        else:
+            object.__setattr__(self, "APP_VERSION", self.APP_VERSION)
+
+        # Database
+        self.DB_FILE         = self.DATA_PATH + "/" + self.DB_FILE
+        self.LOG_FILE        = self.DATA_PATH + "/" + self.LOG_FILE
+        self.LOG_ACCESS_FILE = self.DATA_PATH + "/" + self.LOG_ACCESS_FILE
+
+        # Update DNS Files
+        if self.DOMAIN.lower() != default.DOMAIN.lower():
+            self.DNS_HOST_FILE    = self.DNS_HOST_FILE.replace(default.DOMAIN, self.DOMAIN)
+            self.DNS_ALIAS_FILE   = self.DNS_ALIAS_FILE.replace(default.DOMAIN, self.DOMAIN)
+
+# ---------------------------------------------------------
+# Singleton
+# ---------------------------------------------------------
+settings = Settings()
diff --git a/log/__init__.py b/log/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/log/log.py b/log/log.py
deleted file mode 100644 (file)
index 899b179..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-# log/log.py
-
-from __future__ import annotations
-
-import logging
-import logging.config
-import os
-from typing import Optional
-
-# Module-level flag to prevent re-initialization
-_INITIALIZED = False
-
-# ---------------------------------------------------------
-# Build a full dictConfig for logging
-# ---------------------------------------------------------
-def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None,) -> dict:
-    """
-    Returns a complete dictConfig for the logging system, including formatters,
-    console/file handlers, and specific loggers (uvicorn, fastapi, etc).
-    """
-    level = (level or "INFO").upper()
-
-    formatters = {
-        # Generic Formatter
-        "detailed": {
-            "format": "%(asctime)s %(levelname)s [%(name)s] %(message)s",
-            "datefmt": "%Y-%m-%dT%H:%M:%S%z",
-        },
-        # Access log Formatter
-        "access": {
-            "()": "uvicorn.logging.AccessFormatter",
-            "fmt": '%(asctime)s %(levelname)s [%(name)s] %(client_addr)s - "%(request_line)s" %(status_code)s',
-            "datefmt": "%Y-%m-%dT%H:%M:%S%z",
-        },
-    }
-
-    handlers = {
-        # Generic Console (root/uvicorn/uvicorn.error/fastapi/app)
-        "console": {
-            "class": "logging.StreamHandler",
-            "level": level,
-            "formatter": "detailed",
-            "stream": "ext://sys.stdout",
-        },
-        # Access log Console
-        "access_console": {
-            "class": "logging.StreamHandler",
-            "level": level,
-            "formatter": "access",
-            "stream": "ext://sys.stdout",
-        },
-    }
-
-    # Select active handler based on console
-    active_handlers = ["console"]
-    access_handlers = ["access_console"]
-
-    if to_file:
-        if log_file is not None:
-            # Ensure the log directory exists and add a rotating file handler
-            log_dir = os.path.dirname(log_file) or "."
-            os.makedirs(log_dir, exist_ok=True)
-            # handler for generic log
-            handlers["file"] = {
-                "class": "logging.handlers.RotatingFileHandler",
-                "level": level,
-                "formatter": "detailed",
-                "filename": log_file,
-                "maxBytes": 5 * 1024 * 1024,
-                "backupCount": 5,
-                "encoding": "utf-8",
-            }
-            # Add active handler for generic log file
-            active_handlers.append("file")
-        if log_access_file is not None:
-            # Ensure the log directory exists and add a rotating file handler
-            log_dir = os.path.dirname(log_access_file) or "."
-            os.makedirs(log_dir, exist_ok=True)
-            # handler for access log
-            handlers["access_file"] = {
-                "class": "logging.handlers.RotatingFileHandler",
-                "level": level,
-                "formatter": "access",
-                "filename": log_access_file,
-                "maxBytes": 5 * 1024 * 1024,
-                "backupCount": 5,
-                "encoding": "utf-8",
-            }
-            # Add active handler for access log file
-            access_handlers.append("access_file")
-
-    return {
-        "version": 1,
-        "disable_existing_loggers": False,
-        "formatters": formatters,
-        "handlers": handlers,
-
-        # Root logger
-        "root": {
-            "level": level,
-            "handlers": active_handlers,
-        },
-        # Modules
-        "loggers": {
-            # Uvicorn core
-            "uvicorn": {
-                "level": level,
-                "handlers": active_handlers,
-                "propagate": False,
-            },
-            # Uvicorn internal error
-            "uvicorn.error": {
-                "level": level,
-                "handlers": active_handlers,
-                "propagate": False,
-            },
-            # Uvicorn access log
-            "uvicorn.access": {
-                "level": level,
-                "handlers": access_handlers,
-                "propagate": False,
-            },
-            # FastAPI
-            "fastapi": {
-                "level": level,
-                "handlers": active_handlers,
-                "propagate": False,
-            },
-            # Logger applicativo di comodo (puoi usarlo come logging.getLogger("main"))
-            "main": {
-                "level": level,
-                "handlers": active_handlers,
-                "propagate": False,
-            },
-        },
-    }
-
-# ---------------------------------------------------------
-# Initialize logging once (singleton guard)
-# ---------------------------------------------------------
-def setup_logging(level: str = "INFO", to_file: bool = False, log_file: Optional[str] = None, log_access_file: Optional[str] = None, *, force: bool = False) -> None:
-    """
-    Initializes the logging system only once. Subsequent calls are no-ops.
-    Useful to prevent duplicated handlers or reconfiguration side effects.
-    """
-    global _INITIALIZED
-
-    if _INITIALIZED and not force:
-        return
-
-    config = build_log_config(level=level, to_file=to_file, log_file=log_file, log_access_file=log_access_file)
-    logging.config.dictConfig(config)
-
-    _INITIALIZED = True
-
-# ---------------------------------------------------------
-# Get a configured logger for the given module/name
-# ---------------------------------------------------------
-def get_logger(name: str | None = None) -> logging.Logger:
-    """
-    Returns a logger instance configured via the module setup. If setup was not
-    called yet, it falls back to the standard logging defaults.
-    """
-    return logging.getLogger(name or "main")
diff --git a/settings/__init__.py b/settings/__init__.py
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/settings/config.py b/settings/config.py
deleted file mode 100644 (file)
index 052f689..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-# backend/config.py
-
-# ---------------------------------------------------------
-# APP
-# ---------------------------------------------------------
-APP_NAME = "network-manager"
-APP_VERSION = "0.0.2"
diff --git a/settings/default.py b/settings/default.py
deleted file mode 100644 (file)
index 2caf0e9..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-# backend/default.py
-
-# ---------------------------------------------------------
-# Frontend
-# ---------------------------------------------------------
-FRONTEND_DIR = "/app/frontend"
-
-# ---------------------------------------------------------
-# Data Path (DB + Backup)
-# ---------------------------------------------------------
-DATA_PATH = "/data"
-
-# ---------------------------------------------------------
-# Database
-# ---------------------------------------------------------
-DB_FILE = "database.db"
-DB_RESET = False
-
-# ---------------------------------------------------------
-# Log
-# ---------------------------------------------------------
-LOG_LEVEL = "INFO"
-LOG_TO_FILE = False
-LOG_FILE = "app.log"
-LOG_ACCESS_FILE = "access.log"
-
-# ---------------------------------------------------------
-# Host
-# ---------------------------------------------------------
-DOMAIN = "example.com"
-EXTERNAL_NAME = "dyndns.example.com"
-
-# ---------------------------------------------------------
-# Web
-# ---------------------------------------------------------
-HTTP_HOST = "0.0.0.0"
-HTTP_PORT = "8000"
-LOGIN_MAX_ATTEMPTS = "5"
-LOGIN_WINDOW_SECONDS = "600"
-
-# ---------------------------------------------------------
-# Admin
-# ---------------------------------------------------------
-ADMIN_USER = "admin"
-ADMIN_PASSWORD = "admin"
-ADMIN_PASSWORD_HASH_FILE = "/run/secrets/admin_password_hash"
-
-# ---------------------------------------------------------
-# DNS
-# ---------------------------------------------------------
-DNS_HOST_FILE=f"/dns/etc/{DOMAIN}/hosts.inc"
-DNS_ALIAS_FILE=f"/dns/etc/{DOMAIN}/alias.inc"
-DNS_REVERSE_FILE="/dns/etc/reverse/hosts.inc"
-
-# ---------------------------------------------------------
-# DHCP
-# ---------------------------------------------------------
-DHCP4_HOST_FILE="/dhcp/etc/hosts-ipv4.json"
-DHCP4_LEASES_FILE="/dhcp/lib/dhcp4.leases"
-DHCP6_HOST_FILE="/dhcp/etc/hosts-ipv6.json"
-DHCP6_LEASES_FILE="/dhcp/lib/dhcp6.leases"
diff --git a/settings/settings.py b/settings/settings.py
deleted file mode 100644 (file)
index 69e5e76..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-# backend/settings.py
-
-from __future__ import annotations
-
-# import standard modules
-import os
-import secrets
-import datetime
-from pathlib import Path
-from typing import Optional
-from pydantic import BaseModel, Field, field_validator
-# Import Parameters
-from . import config, default
-
-# ---------------------------------------------------------
-# Convert value to boolean
-# ---------------------------------------------------------
-def _to_bool(val) -> bool:
-    if isinstance(val, bool):
-        return val
-    if val is None:
-        return False
-    return str(val).strip().lower() in {"1", "true", "yes", "on"}
-
-# ---------------------------------------------------------
-# Read text from file if it exists
-# ---------------------------------------------------------
-def _read_text_if_exists(path: Optional[str]) -> Optional[str]:
-    if not path:
-        return None
-    p = Path(path)
-    if p.exists() and p.is_file():
-        try:
-            return p.read_text(encoding="utf-8").strip()
-        except Exception:
-            return None
-    return None
-
-# ---------------------------------------------------------
-# Settings Model
-# ---------------------------------------------------------
-class Settings(BaseModel):
-    # Naming
-    APP_NAME: str = Field(default_factory=lambda: config.APP_NAME)
-
-    # Versioning
-    APP_VERSION: str = Field(default_factory=lambda: config.APP_VERSION)
-    DEVEL: bool = Field(default_factory=lambda: _to_bool(os.getenv("DEV", False)))
-
-    # DATA_PATH
-    DATA_PATH: str = Field(default_factory=lambda: os.getenv("DATA_PATH", default.DATA_PATH))
-
-    # Frontend
-    FRONTEND_DIR: str = Field(default_factory=lambda: os.getenv("FRONTEND_DIR", default.FRONTEND_DIR))
-
-    # Database
-    DB_FILE: str = Field(default_factory=lambda: os.getenv("DB_FILE", default.DB_FILE))
-    DB_RESET: bool = Field(default_factory=lambda: _to_bool(os.getenv("DB_RESET", default.DB_RESET)))
-
-    # Log
-    LOG_LEVEL: str = Field(default_factory=lambda: os.getenv("LOG_LEVEL", default.LOG_LEVEL))
-    LOG_TO_FILE: bool = Field(default_factory=lambda: _to_bool(os.getenv("LOG_TO_FILE", default.LOG_TO_FILE)))
-    LOG_FILE: str = Field(default_factory=lambda: os.getenv("LOG_FILE", default.LOG_FILE))
-    LOG_ACCESS_FILE: str = Field(default_factory=lambda: os.getenv("LOG_ACCESS_FILE", default.LOG_ACCESS_FILE))
-
-    # Hosts
-    DOMAIN: str = Field(default_factory=lambda: os.getenv("DOMAIN", default.DOMAIN))
-    EXTERNAL_NAME: str = Field(default_factory=lambda: os.getenv("EXTERNAL_NAME", default.DOMAIN))
-
-    # Web
-    HTTP_HOST: str = Field(default_factory=lambda: os.getenv("HTTP_HOST", default.HTTP_HOST))
-    HTTP_PORT: int = Field(default_factory=lambda: int(os.getenv("HTTP_PORT", default.HTTP_PORT)))
-    SECRET_KEY: str = Field(default_factory=lambda: (
-        (os.getenv("SESSION_SECRET") or _read_text_if_exists(os.getenv("SECRET_KEY_FILE")) or secrets.token_urlsafe(64)).strip()
-    ))
-    LOGIN_MAX_ATTEMPTS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_MAX_ATTEMPTS", default.LOGIN_MAX_ATTEMPTS)))
-    LOGIN_WINDOW_SECONDS: int = Field(default_factory=lambda: int(os.getenv("LOGIN_WINDOW_SECONDS", default.LOGIN_WINDOW_SECONDS)))
-
-    # Admin
-    ADMIN_USER: str = Field(default_factory=lambda: os.getenv("ADMIN_USER", default.ADMIN_USER))
-    ADMIN_PASSWORD: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD", default.ADMIN_PASSWORD))
-    ADMIN_PASSWORD_HASH_FILE: str = Field(default_factory=lambda: os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE))
-    ADMIN_PASSWORD_HASH: Optional[str] = Field(default_factory=lambda: (
-        (os.getenv("ADMIN_PASSWORD_HASH") or _read_text_if_exists(os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)) or None)
-    ))
-
-    # DNS
-    DNS_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DNS_HOST_FILE", default.DNS_HOST_FILE))
-    DNS_ALIAS_FILE: str = Field(default_factory=lambda: os.getenv("DNS_ALIAS_FILE", default.DNS_ALIAS_FILE))
-    DNS_REVERSE_FILE: str = Field(default_factory=lambda: os.getenv("DNS_REVERSE_FILE", default.DNS_REVERSE_FILE))
-    # DHCP
-    DHCP4_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_HOST_FILE", default.DHCP4_HOST_FILE))
-    DHCP4_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP4_LEASES_FILE", default.DHCP4_LEASES_FILE))
-    DHCP6_HOST_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_HOST_FILE", default.DHCP6_HOST_FILE))
-    DHCP6_LEASES_FILE: str = Field(default_factory=lambda: os.getenv("DHCP6_LEASES_FILE", default.DHCP6_LEASES_FILE))
-
-    def model_post_init(self, __context) -> None:
-        if self.DEVEL:
-            ts = datetime.datetime.now().strftime("%Y%m%d-%H%M")
-            object.__setattr__(self, "APP_VERSION", f"{self.APP_VERSION}-dev-{ts}")
-        else:
-            object.__setattr__(self, "APP_VERSION", self.APP_VERSION)
-
-        # Database
-        self.DB_FILE         = self.DATA_PATH + "/" + self.DB_FILE
-        self.LOG_FILE        = self.DATA_PATH + "/" + self.LOG_FILE
-        self.LOG_ACCESS_FILE = self.DATA_PATH + "/" + self.LOG_ACCESS_FILE
-
-        # Update DNS Files
-        if self.DOMAIN.lower() != default.DOMAIN.lower():
-            self.DNS_HOST_FILE    = self.DNS_HOST_FILE.replace(default.DOMAIN, self.DOMAIN)
-            self.DNS_ALIAS_FILE   = self.DNS_ALIAS_FILE.replace(default.DOMAIN, self.DOMAIN)
-
-# ---------------------------------------------------------
-# Singleton
-# ---------------------------------------------------------
-settings = Settings()