]> git.giorgioravera.it Git - network-manager.git/commitdiff
Improvements in setting class
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 29 May 2026 11:06:54 +0000 (13:06 +0200)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Fri, 29 May 2026 11:06:54 +0000 (13:06 +0200)
17 files changed:
README.md
backend/app.py
backend/bootstrap.py
backend/db/db.py
backend/db/leases.py
backend/log/log.py
backend/routes/aliases.py
backend/routes/devices.py
backend/routes/dhcp.py
backend/routes/dns.py
backend/routes/hosts.py
backend/routes/login.py
backend/server.py
backend/settings/default.py
backend/settings/settings.py
backend/utils.py
frontend/css/variables.css

index 12e1a470454aebcdea2b16b2073afe16dfb62734..bf769a01597df05dc2098a2713748a7960093ac6 100644 (file)
--- a/README.md
+++ b/README.md
@@ -30,12 +30,12 @@ This project is currently under development. For upcoming tasks and planned impr
 
 ## ✨ Features
 
-- Static frontend served by the application (`FRONTEND_DIR`)
+- Static frontend served by the application (`FRONTEND_PATH`)
 - Persistent SQLite database (`/data/database.db`)
 - Configurable logging to console and/or file
 - Login protection with configurable rate-limit
 - Admin credentials configurable via env or Docker secrets
-- Support for `SESSION_SECRET`: custom key for cookies (if missing, it is generated automatically)
+- Support for `SESSION_SECRET`: custom key for cookie signing (required in production; auto-generated in development if not provided)
 
 ---
 
@@ -76,7 +76,7 @@ LOG_TO_FILE=false
 # --- Session secret (optional but recommended in production) ---
 # SESSION_SECRET=****ReplaceWithYourSecret*****
 ```
-If SESSION_SECRET is not set, the app generates a random key on each restart -> existing sessions become invalid.
+If SESSION_SECRET is not set, the application generates a new random key at each restart, which invalidates all existing sessions.
 
 ### 3) 🐳 Example `docker-compose.yml`
 ```yaml
@@ -89,7 +89,7 @@ services:
       - "${HTTP_PORT:-8000}:8000"
     environment:
       # Frontend
-      FRONTEND_DIR: "/app/frontend"
+      FRONTEND_PATH: "/app/frontend"
       # Database
       DB_FILE: "/data/database.db"
       DB_RESET: "${DB_RESET:-false}"
@@ -126,7 +126,7 @@ secrets:
 ## 🔧 Supported environment variables
 | Variable | Default | Description |
 |----------|---------|-------------|
-| `FRONTEND_DIR` | /app/frontend | Frontend directory |
+| `FRONTEND_PATH` | /app/frontend | Frontend directory |
 | `DATA_PATH`| /data | Data Path for DB and Backups |
 | `DB_FILE` | database.db | SQLite file |
 | `DB_RESET` | false | Reset DB on every startup |
index cf961354eada8bfe6068b980cbbd0515992dc59e..28a1126409778fe42092bf307f48af11ccc9e848 100644 (file)
@@ -7,7 +7,6 @@ from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import FileResponse, RedirectResponse, JSONResponse, Response
 from starlette.middleware.base import BaseHTTPMiddleware
 from typing import Callable
-import os
 
 # Import Routers
 from backend.routes.about import router as about_router
@@ -166,39 +165,39 @@ async def session_middleware(request: Request, call_next):
 # ------------------------------------------------------------------------------
 # Homepage
 def home(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "index.html"))
+    return FileResponse(settings.FRONTEND_PATH / "index.html")
 
 # Homepage JS
 def js_home(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/index.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/index.js")
 
 # Modals
 def modals(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "modals.html"))
+    return FileResponse(settings.FRONTEND_PATH / "modals.html")
 
 # CSS variables
 def css_variables(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/variables.css"))
+    return FileResponse(settings.FRONTEND_PATH / "css/variables.css")
 
 # CSS Layout
 def css_layout(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/layout.css"))
+    return FileResponse(settings.FRONTEND_PATH, "css/layout.css")
 
 # JS Common
 def js_common(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/common.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/common.js")
 
 # JS API
 def js_api(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/api.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/api.js")
 
 # JS Services
 def js_services(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/services.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/services.js")
 
 # favicon
 def favicon(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "favicon.ico"))
+    return FileResponse(settings.FRONTEND_PATH / "favicon.ico")
 
 # ------------------------------------------------------------------------------
 # Creates and configures the FastAPI app
index f184200a4bb328f112bceac87cb5e44633f7a61c..0520048fffeacc484c6884feefacba8167f59bcf 100644 (file)
@@ -2,7 +2,6 @@
 
 # Import standard modules
 import logging
-import os
 
 # Import backend modules
 from backend.db.db import init_db
@@ -10,7 +9,6 @@ import backend.db.config
 import backend.db.users
 import backend.db.hosts
 import backend.db.aliases
-from backend.backup import backup_restore
 
 # Import Settings & Logging
 from backend.settings.settings import settings
@@ -30,19 +28,19 @@ def print_welcome(logger):
     )
     logger.info(
         "App settings: frontend=%s | host=%s | port=%d | secret=%s",
-        settings.FRONTEND_DIR, settings.HTTP_HOST, settings.HTTP_PORT, masked_secret
+        str(settings.FRONTEND_PATH), settings.HTTP_HOST, settings.HTTP_PORT, masked_secret
     )
     logger.info(
         "Database: file=%s | reset=%s",
-        settings.DB_FILE, settings.DB_RESET
+        str(settings.DB_FILE), settings.DB_RESET
     )
     logger.info(
         "Log: level=%s, to_file=%s, file=%s",
-        settings.LOG_LEVEL, settings.LOG_TO_FILE, settings.LOG_FILE
+        settings.LOG_LEVEL, settings.LOG_TO_FILE, str(settings.LOG_FILE)
     )
     logger.info(
         "Users: admin=%s | password=%s | hash=%s | hash_file=%s",
-        settings.ADMIN_USER, masked_admin_pwd, masked_admin_hash, settings.ADMIN_PASSWORD_HASH_FILE
+        settings.ADMIN_USER, masked_admin_pwd, masked_admin_hash, str(settings.ADMIN_PASSWORD_HASH_FILE)
     )
     logger.info(
         "DNS: host file=%s | alias file=%s | reverse file=%s",
@@ -54,7 +52,7 @@ def print_welcome(logger):
     )
     logger.info(
         "Backup: path=%s",
-        settings.BACKUP_PATH
+        str(settings.BACKUP_PATH)
     )
     logger.info(
         "App features: ping_workers=%d",
@@ -74,32 +72,26 @@ def print_goodbye(logger):
 # Create DB if needed
 # ================================
 def create_db(logger):
+    db_path = settings.DB_FILE
+
     # Reset database if requested
-    if settings.DB_RESET and os.path.exists(settings.DB_FILE):
-        logger.info("Removing existing database: %s", settings.DB_FILE)
-        os.remove(settings.DB_FILE)
+    if settings.DB_RESET and db_path.exists():
+        logger.info("Removing existing database: %s", db_path)
+        db_path.unlink()
 
     # Skip creation if DB already exists
-    if os.path.exists(settings.DB_FILE):
+    if db_path.exists():
         logger.info("Database already exists. Nothing to do.")
         return
 
-    logger.info("Creating database: %s", settings.DB_FILE)
+    logger.info("Creating database: %s", db_path)
 
     # Ensure directory exists
-    os.makedirs(os.path.dirname(settings.DB_FILE) or ".", exist_ok=True)
+    db_path.parent.mkdir(parents=True, exist_ok=True)
 
     # Initialize DB tables
     init_db()
 
-    # Restore from backup if requested
-    result = backup_restore(cleanup=False)
-    errors = result.get("errors") or []
-    if errors:
-        logger.warning("Failed restoring database from backup")
-    else:
-        logger.info("Database succesfully restored from backup")
-
 # ------------------------------------------------------------------------------
 # Bootstrap: setup logging, print welcome, create DB, etc.
 # ------------------------------------------------------------------------------
@@ -113,22 +105,3 @@ def bootstrap():
 
     # Create or update database
     create_db(logger)
-
-    #os.makedirs(DATA_DIR, exist_ok=True)
-    #os.makedirs(BIND_DIR, exist_ok=True)
-    #os.makedirs(KEA_DIR, exist_ok=True)
-
-    #if not os.path.exists(DB_PATH):
-    #    conn = sqlite3.connect(DB_PATH)
-    #    # eventuale executescript(schema) qui
-    #    conn.close()
-
-    #named_conf = os.path.join(BIND_DIR, "named.conf")
-    #if not os.path.exists(named_conf):
-    #    with open(named_conf, "w") as f:
-    #        f.write("// generated by network-manager\n")
-
-    #kea_conf = os.path.join(KEA_DIR, "kea-dhcp4.conf")
-    #if not os.path.exists(kea_conf):
-    #    with open(kea_conf, "w") as f:
-    #        f.write("{\n  // generated by network-manager\n}\n")
index 54355f2505bcf23d35c96dcd2b75c8ab9d540fd1..635731b36738e7739fb537db5124ce55a01f9506 100644 (file)
@@ -1,7 +1,6 @@
 # backend/db/db.py
 
 # Import standard modules
-import os
 import sqlite3
 
 # Import Settings & Logging
@@ -27,7 +26,7 @@ def register_init(func):
 def get_db():
     global _connection
     if _connection is None:
-        os.makedirs(os.path.dirname(settings.DB_FILE) or ".", exist_ok=True)
+        settings.DB_FILE.parent.mkdir(parents=True, exist_ok=True)
         _connection = sqlite3.connect(settings.DB_FILE, check_same_thread=False)
         _connection.row_factory = sqlite3.Row
         _connection.execute("PRAGMA foreign_keys = ON;")
index cc3b10b4ded69ee33ddf0bfa03c78f903d80ae14..f7cdd44dc69088deeb3f1548f7b9cb30f59dd2f9 100644 (file)
@@ -2,7 +2,6 @@
 
 # import standard modules
 import csv
-import os
 from pathlib import Path
 from typing import Any, Dict, List, Optional
 
@@ -37,7 +36,7 @@ def get_leases(filter_devices: bool = False) -> List[Dict[str, Any]]:
     leases = []
     index = 1  # 1-based id for frontend
 
-    path = Path(settings.DHCP4_LEASES_FILE)
+    path = settings.DHCP4_LEASES_FILE
     if not path.exists():
         raise FileNotFoundError(f"File not found: {path}")
 
@@ -88,7 +87,7 @@ def get_leases(filter_devices: bool = False) -> List[Dict[str, Any]]:
 # SELECT SINGLE LEASE
 # -----------------------------
 def get_lease(lease_id: int) -> Optional[Dict[str, Any]]:
-    path = Path(settings.DHCP4_LEASES_FILE)
+    path = settings.DHCP4_LEASES_FILE
     if not path.exists():
         raise FileNotFoundError(f"File not found: {path}")
 
@@ -124,7 +123,7 @@ def get_lease(lease_id: int) -> Optional[Dict[str, Any]]:
 # -----------------------------
 def delete_lease(lease_id: int):
 
-    path = Path(settings.DHCP4_LEASES_FILE)
+    path = settings.DHCP4_LEASES_FILE
     if not path.exists():
         raise FileNotFoundError(f"File not found: {path}")
 
index 18f43ab88565b394e9d1d0135cd65e61956b5a7c..70344ebcc21982259500ff66ae759971e5f04d40 100644 (file)
@@ -4,7 +4,6 @@ from __future__ import annotations
 
 import logging
 import logging.config
-import os
 from typing import Optional
 
 # Module-level flag to prevent re-initialization
@@ -58,8 +57,7 @@ def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optio
     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)
+            log_file.parent.mkdir(parents=True, exist_ok=True)
             # handler for generic log
             handlers["file"] = {
                 "class": "logging.handlers.RotatingFileHandler",
@@ -74,8 +72,7 @@ def build_log_config(level: str = "INFO", to_file: bool = False, log_file: Optio
             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)
+            log_access_file.parent.mkdir(parents=True, exist_ok=True)
             # handler for access log
             handlers["access_file"] = {
                 "class": "logging.handlers.RotatingFileHandler",
index 242d559ad6ae4967cded4fc2958fa532be009d61..d3106f7d46818a23a2f07123bdbfedfa274d8197 100644 (file)
@@ -5,7 +5,6 @@ from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
 import ipaddress
 import time
-import os
 
 # Import local modules
 from backend.db.aliases import (
@@ -32,12 +31,12 @@ router = APIRouter()
 # Aliass page
 @router.get("/aliases")
 def aliases(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "aliases.html"))
+    return FileResponse(settings.FRONTEND_PATH / "aliases.html")
 
 # Serve aliases.js
 @router.get("/js/aliases.js")
 def js_aliases():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/aliases.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/aliases.js")
 
 # ---------------------------------------------------------
 # Get Aliass
index d9fd93b060a476566c1cd4cc715403f15dba62d4..05793aefc41dcf38e80d5d7c07d3f412e885319d 100644 (file)
@@ -6,7 +6,6 @@ from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
 import ipaddress
 import time
-import os
 
 # Import local modules
 from backend.db.hosts import get_hosts
@@ -30,12 +29,12 @@ router = APIRouter()
 # Devices page
 @router.get("/devices")
 def devices(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "devices.html"))
+    return FileResponse(settings.FRONTEND_PATH / "devices.html")
 
 # Serve devices.js
 @router.get("/js/devices.js")
 def js_devices():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/devices.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/devices.js")
 
 # ---------------------------------------------------------
 # Get Devices
index feb57544c715874cfd040e06cfe1473a2e940fc5..e777fe1052f1415c2e78931f9b0cbe07141dbf88 100644 (file)
@@ -4,7 +4,6 @@
 from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
 import json
-import os
 import time
 
 # Import local modules
@@ -27,12 +26,12 @@ router = APIRouter()
 # Leases page
 @router.get("/leases")
 def leases(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "leases.html"))
+    return FileResponse(settings.FRONTEND_PATH / "leases.html")
 
 # Serve leases.js
 @router.get("/js/leases.js")
 def js_leases():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/leases.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/leases.js")
 
 # ---------------------------------------------------------
 # Reload
index c93083f9fed952a9058882f672e613133e430333..bd5bff843939fffe5e9427036e9a9e7a6e6c363c 100644 (file)
@@ -5,7 +5,6 @@ from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
 import asyncio
 import json
-import os
 import ipaddress
 import time
 
@@ -81,7 +80,9 @@ async def api_dns_reload(request: Request):
         ext_cname = get_config("external_name")
 
         # Save DNS Host and Aliases for the EXT DNS
-        path = settings.DNS_HOST_FILE + "_ext"
+        path = settings.DNS_HOST_FILE.with_name(
+            settings.DNS_HOST_FILE.name + "_ext"
+        )
         with open(path, "w", encoding="utf-8") as f:
             for h in hosts:
                 name   = h.get("name").ljust(20)
index 577f2fad0c011f5082460c7269cc9dd3f1a08059..bbdf9e41aa242428d4fdf8ce492d877239a6b8c6 100644 (file)
@@ -5,7 +5,6 @@ from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
 import ipaddress
 import time
-import os
 
 # Import local modules
 from backend.db.hosts import (
@@ -32,12 +31,12 @@ router = APIRouter()
 # Hosts page
 @router.get("/hosts")
 def hosts(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "hosts.html"))
+    return FileResponse(settings.FRONTEND_PATH / "hosts.html")
 
 # Serve hosts.js
 @router.get("/js/hosts.js")
 def js_hosts():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/hosts.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/hosts.js")
 
 # ---------------------------------------------------------
 # Get Hosts
index 8978aa957c0942fabdc3ff7446aea960edc81eac..baec3af8e8615f1c4486ef224983e1b5026818af 100644 (file)
@@ -3,7 +3,6 @@
 # import standard modules
 from fastapi import APIRouter, Request, Response, HTTPException, status
 from fastapi.responses import FileResponse
-import os
 import time
 
 # Import local modules
@@ -45,17 +44,17 @@ def check_rate_limit(ip: str):
 # Login page
 @router.get("/login")
 def login_page(request: Request):
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "login.html"))
+    return FileResponse(settings.FRONTEND_PATH / "login.html")
 
 # Serve login.js
 @router.get("/js/login.js")
 def css_login():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/login.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/login.js")
 
 # Serve session.js
 @router.get("/js/session.js")
 def css_login():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/session.js"))
+    return FileResponse(settings.FRONTEND_PATH / "js/session.js")
 
 # ---------------------------------------------------------
 # Login API
index 084c1c1dd802de6da808f5c3457b7740b25ce432..3cbc267301a41258f5c9db3da2d046437606152b 100644 (file)
@@ -17,7 +17,7 @@ def run_server(app):
 
     # Uvicorn config da settings with fallback
     host=(settings.HTTP_HOST or "0.0.0.0")
-    port=int(settings.HTTP_PORT or 8000)
+    port=(settings.HTTP_PORT or 8000)
     log_level=(settings.LOG_LEVEL or "info").lower()
     workers = 1 # GRGR in prod valuta gunicorn+uvicorn workers
     #reload = os.getenv("UVICORN_RELOAD", "false").lower() == "true"
index 3b0ad5078ca5851487263ade480ef07b7eb72412..b7b40f0a9139d2310562c4eb7935fb773c63fe0c 100644 (file)
@@ -3,7 +3,7 @@
 # ---------------------------------------------------------
 # Frontend
 # ---------------------------------------------------------
-FRONTEND_DIR = "/app/frontend"
+FRONTEND_PATH = "/app/frontend"
 
 # ---------------------------------------------------------
 # Data Path (DB + Backup)
@@ -34,9 +34,9 @@ EXTERNAL_NAME = "dyndns.example.com"
 # Web
 # ---------------------------------------------------------
 HTTP_HOST = "0.0.0.0"
-HTTP_PORT = "8000"
-LOGIN_MAX_ATTEMPTS = "5"
-LOGIN_WINDOW_SECONDS = "600"
+HTTP_PORT = 8000
+LOGIN_MAX_ATTEMPTS = 5
+LOGIN_WINDOW_SECONDS = 600
 
 # ---------------------------------------------------------
 # Admin
@@ -48,8 +48,8 @@ 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}/aliases.inc"
+DNS_HOST_FILE = "/dns/etc/{domain}/hosts.inc"
+DNS_ALIAS_FILE = "/dns/etc/{domain}/aliases.inc"
 DNS_REVERSE_FILE="/dns/etc/reverse/hosts.inc"
 
 # ---------------------------------------------------------
index 43a80f2290be02ad7de7bd52ada07bbe947b44c2..e769f744789d81450c31b79dc681bf7b058f4f6a 100644 (file)
@@ -8,20 +8,41 @@ import secrets
 import datetime
 from pathlib import Path
 from typing import Optional
-from pydantic import BaseModel, Field, field_validator
+from pydantic import BaseModel, Field
 
 # Import Parameters
 from . import config, default
+from backend.utils import to_int, to_bool
 
 # ---------------------------------------------------------
-# Convert value to boolean
+# Internal: load secret
 # ---------------------------------------------------------
-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"}
+def _load_secret_key() -> str:
+    key = (
+        os.getenv("SESSION_SECRET")
+        or _read_text_if_exists(os.getenv("SECRET_KEY_FILE"))
+    )
+
+    if not key:
+        if not to_bool(os.getenv("DEV"), False):
+            print("WARNING: SECRET_KEY auto-generated (not safe for production)")
+        key = secrets.token_urlsafe(64)
+
+    return key.strip()
+
+# ---------------------------------------------------------
+# Internal: load admin hash
+# ---------------------------------------------------------
+def _load_admin_hash() -> Optional[str]:
+    env = os.getenv("ADMIN_PASSWORD_HASH")
+    if env:
+        return env
+
+    file_env = os.getenv("ADMIN_PASSWORD_HASH_FILE")
+    if file_env:
+        return _read_text_if_exists(file_env)
+
+    return _read_text_if_exists(default.ADMIN_PASSWORD_HASH_FILE)
 
 # ---------------------------------------------------------
 # Read text from file if it exists
@@ -46,80 +67,96 @@ class Settings(BaseModel):
 
     # Versioning
     APP_VERSION: str = Field(default_factory=lambda: config.APP_VERSION)
-    DEVEL: bool = Field(default_factory=lambda: _to_bool(os.getenv("DEV", False)))
+    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))
+    DATA_PATH: Path = Field(default_factory=lambda: Path(os.getenv("DATA_PATH", default.DATA_PATH)))
 
     # Frontend
-    FRONTEND_DIR: str = Field(default_factory=lambda: os.getenv("FRONTEND_DIR", default.FRONTEND_DIR))
+    FRONTEND_PATH: Path = Field(default_factory=lambda: Path(os.getenv("FRONTEND_PATH", default.FRONTEND_PATH)))
 
     # 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)))
+    DB_FILE: Path = Field(default_factory=lambda: Path(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))
+    LOG_TO_FILE: bool = Field(default_factory=lambda: to_bool(os.getenv("LOG_TO_FILE"), default.LOG_TO_FILE))
+    LOG_FILE: Path = Field(default_factory=lambda: Path(os.getenv("LOG_FILE", default.LOG_FILE)))
+    LOG_ACCESS_FILE: Path = Field(default_factory=lambda: Path(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))
+    EXTERNAL_NAME: str = Field(default_factory=lambda: os.getenv("EXTERNAL_NAME", default.EXTERNAL_NAME))
 
     # 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)))
+    HTTP_PORT: int = Field(default_factory=lambda: to_int(os.getenv("HTTP_PORT"), default.HTTP_PORT))
+    SECRET_KEY: str = Field(default_factory=_load_secret_key)
+    LOGIN_MAX_ATTEMPTS: int = Field(default_factory=lambda: to_int(os.getenv("LOGIN_MAX_ATTEMPTS"), default.LOGIN_MAX_ATTEMPTS))
+    LOGIN_WINDOW_SECONDS: int = Field(default_factory=lambda: to_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)
-    ))
+    ADMIN_PASSWORD_HASH_FILE: Path = Field(default_factory=lambda: Path(os.getenv("ADMIN_PASSWORD_HASH_FILE", default.ADMIN_PASSWORD_HASH_FILE)))
+    ADMIN_PASSWORD_HASH: Optional[str] = Field(default_factory=_load_admin_hash)
 
     # 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))
+    DNS_HOST_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DNS_HOST_FILE", default.DNS_HOST_FILE)))
+    DNS_ALIAS_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DNS_ALIAS_FILE", default.DNS_ALIAS_FILE)))
+    DNS_REVERSE_FILE: Path = Field(default_factory=lambda: Path(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))
+    DHCP4_HOST_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DHCP4_HOST_FILE", default.DHCP4_HOST_FILE)))
+    DHCP4_LEASES_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DHCP4_LEASES_FILE", default.DHCP4_LEASES_FILE)))
+    DHCP6_HOST_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DHCP6_HOST_FILE", default.DHCP6_HOST_FILE)))
+    DHCP6_LEASES_FILE: Path = Field(default_factory=lambda: Path(os.getenv("DHCP6_LEASES_FILE", default.DHCP6_LEASES_FILE)))
 
     # Backup
-    BACKUP_PATH: str= Field(default_factory=lambda: os.getenv("BACKUP_PATH", default.BACKUP_PATH))
+    BACKUP_PATH: Path = Field(default_factory=lambda: Path(os.getenv("BACKUP_PATH", default.BACKUP_PATH)))
 
     # APP Features
-    PING_WORKERS: int = Field(default_factory=lambda: int(os.getenv("PING_WORKERS", default.PING_WORKERS)))
+    PING_WORKERS: int = Field(default_factory=lambda: to_int(os.getenv("PING_WORKERS"), default.PING_WORKERS))
 
+    # ---------------------------------------------------------
+    # Post init process
+    # ---------------------------------------------------------
     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
-
-        # Backup
-        self.BACKUP_PATH = self.BACKUP_PATH if os.path.isabs(self.BACKUP_PATH) else os.path.join(self.DATA_PATH, self.BACKUP_PATH)
 
-        # 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)
+        # Folder Data Creation
+        self.DATA_PATH.mkdir(parents=True, exist_ok=True)
+
+        # Update DB file path including DATA_PATH
+        if not self.DB_FILE.is_absolute():
+            object.__setattr__(self, "DB_FILE", self.DATA_PATH / self.DB_FILE)
+
+        # Update Log files path including DATA_PATH
+        if not self.LOG_FILE.is_absolute():
+            object.__setattr__(self, "LOG_FILE", self.DATA_PATH / self.LOG_FILE)
+        if not self.LOG_ACCESS_FILE.is_absolute():
+            object.__setattr__(self, "LOG_ACCESS_FILE", self.DATA_PATH / self.LOG_ACCESS_FILE)
+
+        # Updated Backup Path
+        if not self.BACKUP_PATH.is_absolute():
+            object.__setattr__(self, "BACKUP_PATH", self.DATA_PATH / self.BACKUP_PATH)
+        self.BACKUP_PATH.parent.mkdir(parents=True, exist_ok=True)
+
+        # Update DNS references based on domain name
+        if "{domain}" in str(self.DNS_HOST_FILE):
+            object.__setattr__(
+                self,
+                "DNS_HOST_FILE",
+                Path(str(self.DNS_HOST_FILE).format(domain=self.DOMAIN)),
+            )
+        if "{domain}" in str(self.DNS_ALIAS_FILE):
+            object.__setattr__(
+                self,
+                "DNS_ALIAS_FILE",
+                Path(str(self.DNS_ALIAS_FILE).format(domain=self.DOMAIN)),
+            )
 
 # ---------------------------------------------------------
 # Singleton
index 2377866ebdd1e4493d8847e46bb4bd7b3c023d25..ae8f706713a9354778cb49fd1d49583f72704d26 100644 (file)
@@ -1,19 +1,8 @@
 # backend/db/utils.py
 
 # Import standard modules
-import os
-import platform
 import subprocess
 
-# -----------------------------
-# Load hash from file
-# -----------------------------
-def load_hash(path: str):
-    if path and os.path.exists(path):
-        with open(path, "r") as f:
-            return f.read().strip()
-    return None
-
 # -----------------------------
 # Normalize string (strip and convert empty to None)
 # -----------------------------
@@ -23,25 +12,25 @@ def normalize(value):
 # -----------------------------
 # convert string to int (returns None if conversion fails)
 # -----------------------------
-def to_int(v: str):
+def to_int(v: str, default: int | None = None) -> int | None:
     v = (v or "").strip()
     if not v or v.lower() == "null":
-        return None
+        return default
     try:
         return int(v)
     except ValueError:
-        return None
+        return default
 
 # -----------------------------
 # convert string to bool (returns None if conversion fails)
 # -----------------------------
-def to_bool(v: str):
+def to_bool(v: str, default: bool | None = None) -> bool | None:
     v = (v or "").strip().lower()
     if v in ("true", "1", "yes", "y"):
         return True
     if v in ("false", "0", "no", "n"):
         return False
-    return None
+    return default
 
 # -----------------------------
 # check if host is active (ping)
index add5ba161cef9bc0ee054e04cee894a875466867..9871a82da5e016c2167129893b0a19a26331764e 100644 (file)
@@ -27,4 +27,4 @@
 
     --bg-selected: rgba(var(--accent-rgb), 0.12);
     --border-selected: rgba(var(--accent-rgb), 0.4);
-}
\ No newline at end of file
+}