From: Giorgio Ravera Date: Fri, 29 May 2026 11:06:54 +0000 (+0200) Subject: Improvements in setting class X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=ebe97d9d4f571782a20c24cb6bb722a0ec9dcf72;p=network-manager.git Improvements in setting class --- diff --git a/README.md b/README.md index 12e1a47..bf769a0 100644 --- 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 | diff --git a/backend/app.py b/backend/app.py index cf96135..28a1126 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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 diff --git a/backend/bootstrap.py b/backend/bootstrap.py index f184200..0520048 100644 --- a/backend/bootstrap.py +++ b/backend/bootstrap.py @@ -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") diff --git a/backend/db/db.py b/backend/db/db.py index 54355f2..635731b 100644 --- a/backend/db/db.py +++ b/backend/db/db.py @@ -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;") diff --git a/backend/db/leases.py b/backend/db/leases.py index cc3b10b..f7cdd44 100644 --- a/backend/db/leases.py +++ b/backend/db/leases.py @@ -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}") diff --git a/backend/log/log.py b/backend/log/log.py index 18f43ab..70344eb 100644 --- a/backend/log/log.py +++ b/backend/log/log.py @@ -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", diff --git a/backend/routes/aliases.py b/backend/routes/aliases.py index 242d559..d3106f7 100644 --- a/backend/routes/aliases.py +++ b/backend/routes/aliases.py @@ -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 diff --git a/backend/routes/devices.py b/backend/routes/devices.py index d9fd93b..05793ae 100644 --- a/backend/routes/devices.py +++ b/backend/routes/devices.py @@ -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 diff --git a/backend/routes/dhcp.py b/backend/routes/dhcp.py index feb5754..e777fe1 100644 --- a/backend/routes/dhcp.py +++ b/backend/routes/dhcp.py @@ -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 diff --git a/backend/routes/dns.py b/backend/routes/dns.py index c93083f..bd5bff8 100644 --- a/backend/routes/dns.py +++ b/backend/routes/dns.py @@ -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) diff --git a/backend/routes/hosts.py b/backend/routes/hosts.py index 577f2fa..bbdf9e4 100644 --- a/backend/routes/hosts.py +++ b/backend/routes/hosts.py @@ -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 diff --git a/backend/routes/login.py b/backend/routes/login.py index 8978aa9..baec3af 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -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 diff --git a/backend/server.py b/backend/server.py index 084c1c1..3cbc267 100644 --- a/backend/server.py +++ b/backend/server.py @@ -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" diff --git a/backend/settings/default.py b/backend/settings/default.py index 3b0ad50..b7b40f0 100644 --- a/backend/settings/default.py +++ b/backend/settings/default.py @@ -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" # --------------------------------------------------------- diff --git a/backend/settings/settings.py b/backend/settings/settings.py index 43a80f2..e769f74 100644 --- a/backend/settings/settings.py +++ b/backend/settings/settings.py @@ -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 diff --git a/backend/utils.py b/backend/utils.py index 2377866..ae8f706 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -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) diff --git a/frontend/css/variables.css b/frontend/css/variables.css index add5ba1..9871a82 100644 --- a/frontend/css/variables.css +++ b/frontend/css/variables.css @@ -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 +}