From: Giorgio Ravera Date: Mon, 5 Jan 2026 18:15:36 +0000 (+0100) Subject: Merged entrypoint with app core X-Git-Tag: v0.0.1~46 X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=70a1af32f17b84f19f41752aa0f85a211ca3ea05;p=network-manager.git Merged entrypoint with app core --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db2fc0d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +secrets diff --git a/Dockerfile b/Dockerfile index efa18e0..c9e1775 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,19 +39,15 @@ WORKDIR /app # Copy application COPY --from=builder /app/backend backend COPY --from=builder /app/frontend frontend -COPY --from=builder /app/entrypoint.py /usr/local/bin/entrypoint.py +COPY --from=builder /app/entrypoint.py entrypoint.py # Ensure Python sees the installed packages ENV PYTHONPATH="/usr/local/lib/python3.12/site-packages" ENV HTTP_PORT=8000 -ENV LOGIN_MAX_ATTEMPTS=5 -ENV LOGIN_WINDOW_SECONDS=600 -ENV DOMAIN=example.com -ENV PUBLIC_IP=127.0.0.1 # Expose the port dynamically EXPOSE ${HTTP_PORT} -ENTRYPOINT ["/usr/local/bin/entrypoint.py"] +ENTRYPOINT ["/app/entrypoint.py"] CMD ["python3", "-u", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..d8c4f36 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,28 @@ +# backend/config.py + +# Import standard modules +import os +import secrets +# Import local modules +from backend.utils import load_hash + +# Database related settings +DB_FILE = os.environ.get("DB_FILE", "/data/database.db") +DB_RESET = os.environ.get("DB_RESET", "0") == "1" + +# Hosts related settings +DOMAIN = os.environ.get("DOMAIN", "example.com") +PUBLIC_IP = os.environ.get("PUBLIC_IP", "127.0.0.1") + +# Web server related settings +SECRET_KEY = os.getenv("SESSION_SECRET", secrets.token_urlsafe(64)) +HTTP_PORT = os.getenv("HTTP_PORT", "8000") +LOGIN_MAX_ATTEMPTS = int(os.getenv("LOGIN_MAX_ATTEMPTS", "5")) +LOGIN_WINDOW_SECONDS = int(os.getenv("LOGIN_WINDOW_SECONDS", "600")) + +# User related settings +ADMIN_USER = os.environ.get("ADMIN_USER", "admin") +ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin") +ADMIN_HASH = os.environ.get("ADMIN_HASH", "") +if not ADMIN_HASH: + ADMIN_HASH = load_hash("ADMIN_HASH_FILE") diff --git a/backend/db/db.py b/backend/db/db.py index f7336f3..ac5e392 100644 --- a/backend/db/db.py +++ b/backend/db/db.py @@ -1,11 +1,17 @@ # backend/db/db.py +# Import standard modules import os import sqlite3 - -DB_PATH = os.environ.get("DB_PATH", "/data/database.db") +# Import local modules +from backend.config import DB_FILE _connection = None +_init_functions = [] + +def register_init(func): + _init_functions.append(func) + return func # ----------------------------- # Connect to the database @@ -13,8 +19,8 @@ _connection = None def get_db(): global _connection if _connection is None: - os.makedirs(os.path.dirname(DB_PATH) or ".", exist_ok=True) - _connection = sqlite3.connect(DB_PATH, check_same_thread=False) + os.makedirs(os.path.dirname(DB_FILE) or ".", exist_ok=True) + _connection = sqlite3.connect(DB_FILE, check_same_thread=False) _connection.row_factory = sqlite3.Row _connection.execute("PRAGMA foreign_keys = ON;") return _connection @@ -23,6 +29,15 @@ def get_db(): # Init Database # ----------------------------- def init_db(): + print(f"INFO: Starting DB Initialization.") + conn = get_db() cur = conn.cursor() + + for func in _init_functions: + func(cur) + conn.commit() + conn.close() + + print(f"INFO: DB Initialization Completed.") \ No newline at end of file diff --git a/backend/db/hosts.py b/backend/db/hosts.py index 44a3744..9f0e98f 100644 --- a/backend/db/hosts.py +++ b/backend/db/hosts.py @@ -1,8 +1,11 @@ # backend/db/hosts.py +# Import standard modules import ipaddress import os +# Import local modules from backend.db.db import get_db +from backend.db.db import register_init # ----------------------------- # SELECT ALL HOSTS @@ -70,3 +73,64 @@ def delete_host(host_id: int): conn.execute("DELETE FROM hosts WHERE id = ?", (host_id,)) conn.commit() return True + +# ----------------------------- +# Initialize Hosts DB Table +# ----------------------------- +@register_init +def init_db_hosts_table(cur): + from backend.config import DOMAIN + from backend.config import PUBLIC_IP + + # GLOBAL SETTINGS + cur.execute(""" + CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT + ); + """) + cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("domain", DOMAIN)) + cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("external_ipv4", PUBLIC_IP)) + + # HOSTS + cur.execute(""" + CREATE TABLE hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + ipv4 TEXT, + ipv6 TEXT, + mac TEXT, + note TEXT, + ssl_enabled INTEGER NOT NULL DEFAULT 0 + ); + """) + cur.execute("CREATE INDEX idx_hosts_name ON hosts(name);") + + # ALIASES + cur.execute(""" + CREATE TABLE aliases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + host_id INTEGER NOT NULL, + alias TEXT NOT NULL, + note TEXT, + ssl_enabled INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (host_id) REFERENCES hosts(id) + ); + """) + cur.execute("CREATE INDEX idx_aliases_host ON aliases(host_id);") + + # TXT RECORDS + cur.execute(""" + CREATE TABLE txt_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + value TEXT NOT NULL, + note TEXT, + host_id INTEGER, + FOREIGN KEY (host_id) REFERENCES hosts(id) + ); + """) + cur.execute("CREATE INDEX idx_txt_host ON txt_records(host_id);") + + print(f"INFO: - HOSTS DB: Database initialized successfully for {DOMAIN}.") + print(f"INFO: - HOSTS DB: Public IP: {PUBLIC_IP}.") diff --git a/backend/db/users.py b/backend/db/users.py index 0b4762a..cc99512 100644 --- a/backend/db/users.py +++ b/backend/db/users.py @@ -1,8 +1,20 @@ # backend/db/users.py +# Import standard modules +import bcrypt import json +import os +# Import local modules from backend.db.db import get_db -from bcrypt import checkpw +from backend.db.db import register_init + +# ================================ +# Create hash password +# ================================ +def hash_password(password: str) -> str: + salt = bcrypt.gensalt(rounds=12) + hashed = bcrypt.hashpw(password.encode(), salt) + return hashed.decode() # ----------------------------- # Get User from DB by username @@ -13,6 +25,55 @@ def get_user_by_username(username): cur.execute("SELECT * FROM users WHERE username = ?", (username,)) return cur.fetchone() +# ----------------------------- +# Create Users Table +# ----------------------------- +@register_init +def init_db_users_table(cur): + from backend.config import ADMIN_USER + from backend.config import ADMIN_PASSWORD + from backend.config import ADMIN_HASH + + cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + email TEXT UNIQUE, + is_admin INTEGER NOT NULL DEFAULT 0, + modules TEXT, -- JSON: ["dns","dhcp","vpn"] + status TEXT NOT NULL DEFAULT 'active', -- active, disabled, locked + failed_attempts INTEGER NOT NULL DEFAULT 0, + last_failed_at INTEGER, + last_login_at INTEGER, + password_changed_at INTEGER, + notes TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + """) + cur.execute("CREATE INDEX idx_users_username ON users(username);") + # Insert default admin user + if not ADMIN_HASH: + ADMIN_HASH = hash_password(ADMIN_PASSWORD) + else: + ADMIN_PASSWORD = "(hidden)" + cur.execute(""" + INSERT INTO users ( + username, password_hash, email, is_admin, modules, status, + created_at, updated_at, password_changed_at + ) VALUES (?, ?, ?, ?, ?, ?, strftime('%s','now'), strftime('%s','now'), strftime('%s','now')); + """, ( + ADMIN_USER, + ADMIN_HASH, + "admin@example.com", + 1, + '["dns","dhcp"]', + "active" + )) + + print(f"INFO: - USERS DB: Admin user: {ADMIN_USER} with password {ADMIN_PASSWORD} - {ADMIN_HASH}.") + # ----------------------------- # Create User # ----------------------------- @@ -46,8 +107,7 @@ def verify_login(username, password): if user["status"] != "active": return False - if not checkpw(password.encode(), user["password_hash"].encode()): + if not bcrypt.checkpw(password.encode(), user["password_hash"].encode()): return False - print(f"DEBUG: Checking password for user {user['username']} with status {user['status']}: {user['password_hash']}") return True diff --git a/backend/main.py b/backend/main.py index 50ad5b0..4b3a9bc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,14 +1,15 @@ +# backend/main.py + +# import standard modules from fastapi import FastAPI from fastapi import Request, Response, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, RedirectResponse from itsdangerous import TimestampSigner -import secrets import os import ipaddress import time - -# Import local models +# Import local modules from backend.db.hosts import ( get_hosts, get_host, @@ -16,16 +17,14 @@ from backend.db.hosts import ( update_host, delete_host ) - from backend.db.users import ( verify_login ) - -# Read Variables -SECRET_KEY = os.getenv("SESSION_SECRET", secrets.token_urlsafe(64)) -HTTP_PORT = os.getenv("HTTP_PORT", "8000") -LOGIN_MAX_ATTEMPTS = int(os.getenv("LOGIN_MAX_ATTEMPTS", "5")) -LOGIN_WINDOW_SECONDS = int(os.getenv("LOGIN_WINDOW_SECONDS", "600")) +# Import config variables +from backend.config import SECRET_KEY +from backend.config import HTTP_PORT +from backend.config import LOGIN_MAX_ATTEMPTS +from backend.config import LOGIN_WINDOW_SECONDS # IP → lista timestamp tentativi login_attempts = {} diff --git a/backend/models.py b/backend/models.py deleted file mode 100644 index 3b8107c..0000000 --- a/backend/models.py +++ /dev/null @@ -1,20 +0,0 @@ -import sqlite3 -import os - -DB_PATH = os.environ.get("DB_PATH", "/app/database.db") - -def init_db(): - conn = sqlite3.connect(DB_PATH) - conn.execute(""" - CREATE TABLE IF NOT EXISTS hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - ipv4 TEXT, - ipv6 TEXT, - mac TEXT, - note TEXT, - ssl_enabled INTEGER NOT NULL DEFAULT 0 - ) - """) - conn.commit() - conn.close() diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 0000000..1551249 --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,14 @@ +# backend/db/utils.py + +# Import standard modules +import os + +# ----------------------------- +# Load hash from file +# ----------------------------- +def load_hash(hash_file: str): + path = os.environ.get(hash_file) + if path and os.path.exists(path): + with open(path, "r") as f: + return f.read().strip() + return None diff --git a/entrypoint.py b/entrypoint.py index 806c9e0..1e2fd80 100755 --- a/entrypoint.py +++ b/entrypoint.py @@ -1,41 +1,26 @@ #!/usr/local/bin/python3 -import bcrypt +# Import standard modules import os import sys -import sqlite3 -import subprocess +# Import local modules +from backend.db.db import init_db +import backend.db.hosts +import backend.db.users # ================================ # Variables # ================================ -DB_FILE = os.environ.get("DB_PATH", "/data/database.db") -DB_RESET = os.environ.get("DB_RESET", "0") == "1" -DOMAIN = os.environ.get("DOMAIN", "example.com") -PUBLIC_IP = os.environ.get("PUBLIC_IP", "127.0.0.1") -ADMIN_USER = os.environ.get("ADMIN_USER", "admin") -ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD", "admin") -ADMIN_HASH = os.environ.get("ADMIN_HASH", "") - IMAGE_NAME = "network-manager-distroless" IMAGE_VERSION = "1.0" -import bcrypt - -# ================================ -# Create hash password -# ================================ -def hash_password(password: str) -> str: - salt = bcrypt.gensalt(rounds=12) - hashed = bcrypt.hashpw(password.encode(), salt) - return hashed.decode() +from backend.config import DB_FILE +from backend.config import DB_RESET # ================================ # Create DB if needed # ================================ -def create_db(): - global DB_FILE, DB_RESET, DOMAIN, PUBLIC_IP, ADMIN_USER, ADMIN_PASSWORD, ADMIN_HASH - +def docker_create_db(): # Reset database if requested if DB_RESET and os.path.exists(DB_FILE): print("INFO: Removing existing database.") @@ -51,107 +36,8 @@ def create_db(): # Ensure directory exists os.makedirs(os.path.dirname(DB_FILE) or ".", exist_ok=True) - conn = sqlite3.connect(DB_FILE) - cur = conn.cursor() - - # Enable foreign keys - cur.execute("PRAGMA foreign_keys = ON;") - - # GLOBAL SETTINGS - cur.execute(""" - CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT - ); - """) - cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("domain", DOMAIN)) - cur.execute("INSERT INTO settings (key, value) VALUES (?, ?)", ("external_ipv4", PUBLIC_IP)) - - # HOSTS - cur.execute(""" - CREATE TABLE hosts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - ipv4 TEXT, - ipv6 TEXT, - mac TEXT, - note TEXT, - ssl_enabled INTEGER NOT NULL DEFAULT 0 - ); - """) - cur.execute("CREATE INDEX idx_hosts_name ON hosts(name);") - - # ALIASES - cur.execute(""" - CREATE TABLE aliases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - host_id INTEGER NOT NULL, - alias TEXT NOT NULL, - note TEXT, - ssl_enabled INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (host_id) REFERENCES hosts(id) - ); - """) - cur.execute("CREATE INDEX idx_aliases_host ON aliases(host_id);") - - # TXT RECORDS - cur.execute(""" - CREATE TABLE txt_records ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - value TEXT NOT NULL, - note TEXT, - host_id INTEGER, - FOREIGN KEY (host_id) REFERENCES hosts(id) - ); - """) - cur.execute("CREATE INDEX idx_txt_host ON txt_records(host_id);") - - # Users RECORDS - cur.execute(""" - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - email TEXT UNIQUE, - is_admin INTEGER NOT NULL DEFAULT 0, - modules TEXT, -- JSON: ["dns","dhcp","vpn"] - status TEXT NOT NULL DEFAULT 'active', -- active, disabled, locked - failed_attempts INTEGER NOT NULL DEFAULT 0, - last_failed_at INTEGER, - last_login_at INTEGER, - password_changed_at INTEGER, - notes TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - """) - cur.execute("CREATE INDEX idx_users_username ON users(username);") - # Insert default admin user - if not ADMIN_HASH: - ADMIN_HASH = hash_password(ADMIN_PASSWORD) - else: - ADMIN_PASSWORD = "(hidden)" - cur.execute(""" - INSERT INTO users ( - username, password_hash, email, is_admin, modules, status, - created_at, updated_at, password_changed_at - ) VALUES (?, ?, ?, ?, ?, ?, strftime('%s','now'), strftime('%s','now'), strftime('%s','now')); - """, ( - ADMIN_USER, - ADMIN_HASH, - "admin@example.com", - 1, - '["dns","dhcp"]', - "active" - )) - - conn.commit() - conn.close() - - print(f"INFO: Database initialized successfully for {DOMAIN}.") - print(f"INFO: Admin user: {ADMIN_USER} with password {ADMIN_PASSWORD} - {ADMIN_HASH}.") - print(f"INFO: Public IP: {PUBLIC_IP}.") + # Initialize all registered DB tables + init_db() # ================================ # Entry Point @@ -182,7 +68,7 @@ while i < len(args): break # Create DB -create_db() +docker_create_db() # Continue to CMD if not args: