# Copy full application
COPY backend backend
COPY frontend frontend
-COPY log log
-COPY settings settings
# ---------- STAGE 2: Alpine Runtime ----------
FROM python:3.12-alpine
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__)
# 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
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__)
--- /dev/null
+# 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")
# 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__)
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__)
@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 (
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__)
--- /dev/null
+# 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")
# Import standard modules
import os
+
# Import backend modules
from backend.bootstrap import bootstrap
from backend.app import create_app
# 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()
# ---------------------------------------------------------
@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,
}
import ipaddress
import time
import os
+
# Import local modules
from backend.db.aliases import (
get_aliases,
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__)
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__)
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__)
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__)
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:
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
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__)
import ipaddress
import time
import os
+
# Import local modules
from backend.db.hosts import (
get_hosts,
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__)
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()
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__)
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__)
--- /dev/null
+# backend/settings/config.py
+
+# ---------------------------------------------------------
+# APP
+# ---------------------------------------------------------
+APP_NAME = "network-manager"
+APP_VERSION = "0.0.2"
--- /dev/null
+# 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"
--- /dev/null
+# 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()
+++ /dev/null
-# 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")
+++ /dev/null
-# backend/config.py
-
-# ---------------------------------------------------------
-# APP
-# ---------------------------------------------------------
-APP_NAME = "network-manager"
-APP_VERSION = "0.0.2"
+++ /dev/null
-# 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"
+++ /dev/null
-# 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()