From: Giorgio Ravera Date: Sat, 3 Jan 2026 21:28:00 +0000 (+0100) Subject: Added login (to be compleated now only with static user) X-Git-Tag: v0.0.1~52 X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=437bb8211d148b919441bb2ba683ed9a5b89da13;p=network-manager.git Added login (to be compleated now only with static user) --- diff --git a/Dockerfile b/Dockerfile index ca092c9..83a4e1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,8 +6,9 @@ WORKDIR /var/www/network-manager # Install system dependencies RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/* -# Install dependencies -RUN pip install --no-cache-dir fastapi uvicorn[standard] +# Install python dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt # Copy backend and frontend COPY backend/ /var/www/network-manager/backend/ diff --git a/backend/main.py b/backend/main.py index 498653f..21e892e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,10 +1,13 @@ from fastapi import FastAPI +from fastapi import Request, Response, HTTPException from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse +from fastapi.responses import FileResponse, RedirectResponse +from itsdangerous import TimestampSigner +import secrets import os import ipaddress -# Import models +# Import local models from backend.db import ( get_hosts, get_host, @@ -23,6 +26,19 @@ app.add_middleware( allow_headers=["*"], ) +# Token signer for session management +SECRET_KEY = os.getenv("SESSION_SECRET", secrets.token_urlsafe(64)) +signer = TimestampSigner(SECRET_KEY) + +def require_login(request: Request): + token = request.cookies.get("session") + if not token: + raise HTTPException(status_code=401, detail="Not authenticated") + try: + signer.unsign(token, max_age=86400) + except: + raise HTTPException(status_code=401, detail="Invalid session") + # --------------------------------------------------------- # FRONTEND PATHS (absolute paths inside Docker) # --------------------------------------------------------- @@ -31,13 +47,42 @@ FRONTEND_DIR = "/var/www/network-manager/frontend" # Homepage @app.get("/") -def index(): - return FileResponse(os.path.join(FRONTEND_DIR, "index.html")) - -# Serve style.css -@app.get("/style.css") -def css(): - return FileResponse(os.path.join(FRONTEND_DIR, "style.css")) +def home(request: Request): + token = request.cookies.get("session") + if not token: + return RedirectResponse("/login") + try: + signer.unsign(token, max_age=86400) # 24h + except: + return RedirectResponse("/login") + return FileResponse(os.path.join(FRONTEND_DIR, "hosts.html")) + +# Login +@app.get("/login") +def login_page(): + return FileResponse(os.path.join(FRONTEND_DIR, "login.html")) + +# Hosts management +@app.get("/hosts") +def hosts(request: Request): + token = request.cookies.get("session") + if not token: + return RedirectResponse("/login") + try: + signer.unsign(token, max_age=86400) + except: + return RedirectResponse("/login") + return FileResponse(os.path.join(FRONTEND_DIR, "hosts.html")) + +# Serve hosts.css +@app.get("/css/hosts.css") +def css_hosts(): + return FileResponse(os.path.join(FRONTEND_DIR, "css/hosts.css")) + +# Serve login.css +@app.get("/css/login.css") +def css_login(): + return FileResponse(os.path.join(FRONTEND_DIR, "css/login.css")) # Serve app.js @app.get("/app.js") @@ -48,65 +93,80 @@ def js(): # API ENDPOINTS # --------------------------------------------------------- +@app.post("/api/login") +def api_login(data: dict, response: Response): + user = data.get("username") + pwd = data.get("password") + if user == "admin" and pwd == "admin": + token = signer.sign(user).decode() + response.set_cookie( + "session", + token, + httponly=True, + max_age=86400, + path="/" + ) + return {"status": "ok"} + return {"error": "Invalid credentials"} + +@app.post("/api/logout") +def api_logout(response: Response): + response.delete_cookie("session") + return {"status": "logged_out"} + @app.get("/api/hosts") -def api_get_hosts(): +def api_get_hosts(request: Request): + require_login(request) return get_hosts() @app.post("/api/hosts") -def api_add_host(data: dict): +def api_add_host(request: Request, data: dict): + require_login(request) name = data.get("name", "").strip() ipv4 = data.get("ipv4") ipv6 = data.get("ipv6") - - # Check input if not name: return {"error": "Name is required"} - - # Validate IPv4 if ipv4: try: ipaddress.IPv4Address(ipv4) except: return {"error": "Invalid IPv4 format"} - - # Validate IPv6 if ipv6: try: ipaddress.IPv6Address(ipv6) except: return {"error": "Invalid IPv6 format"} - return {"id": add_host(data)} @app.get("/api/hosts/{host_id}") -def api_get_host(host_id: int): +def api_get_host(request: Request, host_id: int): + require_login(request) return get_host(host_id) or {} @app.put("/api/hosts/{host_id}") -def api_update_host(host_id: int, data: dict): +def api_update_host(request: Request, data: dict, host_id: int): + require_login(request) name = data.get("name", "").strip() ipv4 = data.get("ipv4") ipv6 = data.get("ipv6") - if not name: return {"error": "Name is required"} - if ipv4: try: ipaddress.IPv4Address(ipv4) except: return {"error": "Invalid IPv4 format"} - if ipv6: try: ipaddress.IPv6Address(ipv6) except: return {"error": "Invalid IPv6 format"} - update_host(host_id, data) return {"status": "ok"} @app.delete("/api/hosts/{host_id}") -def api_delete_host(host_id: int): +def api_delete_host(request: Request, host_id: int): + require_login(request) delete_host(host_id) return {"status": "ok"} diff --git a/frontend/app.js b/frontend/app.js index 89bfa03..8d33d86 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -266,6 +266,46 @@ function resetSorting() { arrows.forEach(a => a.textContent = ""); } +// ----------------------------- +// Login function +// ----------------------------- +async function handleLogin(e) { + e.preventDefault(); + + const user = document.getElementById("username").value.trim(); + const pass = document.getElementById("password").value; + + const res = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ + username: user, + password: pass + }) + }); + + const data = await res.json(); + + if (data.status === "ok") { + window.location.href = "/hosts"; // ora funziona davvero + } else { + document.getElementById("loginError").textContent = "Wrong credentials"; + } +} + +// ----------------------------- +// Logout function +// ----------------------------- +async function handleLogout() { + await fetch("/api/logout", { + method: "POST", + credentials: "include" // 🔥 fondamentale per cancellare il cookie + }); + + window.location.href = "/login"; +} + // ----------------------------- // INITIAL TABLE LOAD // ----------------------------- @@ -278,4 +318,11 @@ document.addEventListener("keydown", (e) => { resetSorting(); clearSearch(); } -}); \ No newline at end of file +}); + +document.addEventListener("DOMContentLoaded", () => { + const logoutBtn = document.getElementById("logoutBtn"); + if (logoutBtn) { + logoutBtn.addEventListener("click", handleLogout); + } +}); diff --git a/frontend/css/hosts.css b/frontend/css/hosts.css new file mode 100644 index 0000000..b36e0df --- /dev/null +++ b/frontend/css/hosts.css @@ -0,0 +1,332 @@ +/* ================================ + Global color variables (pfSense style) + ================================ */ +:root { + --accent: #4da3ff; + --accent-hover: #1f8bff; + + --bg-dark: #111; + --bg-light: #f9f9f9; + --bg-frame: #e8e8e8; + + --text-light: #e6e6e6; + --text-dark: #222; + + --border-light: #ccc; +} + +/* ================================ + Global layout + ================================ */ +body { + font-family: sans-serif; + margin: 20px; + padding-top: 60px; /* space for fixed topbar */ + background-color: var(--bg-light); +} + +/* ================================ + Topbar (pfSense full-width header) + ================================ */ +.topbar { + width: 100%; + background: var(--bg-dark); + padding: 0; + display: flex; + align-items: center; + border-bottom: 3px solid var(--accent); + position: fixed; + top: 0; + left: 0; + z-index: 1000; +} + +.topbar-inner { + width: 100%; + margin: 0 auto; + padding: 14px 26px; + display: flex; + justify-content: space-between; + align-items: center; + box-sizing: border-box; +} + +.logo { + display: flex; + align-items: center; + gap: 14px; +} + +.logo span { + font-size: 1.6rem; + font-weight: 600; + color: var(--text-light); + letter-spacing: 0.5px; +} + +.logo svg { + filter: drop-shadow(0 0 2px rgba(0,0,0,0.6)); +} + +/* ================================ + Page frame (section header) + ================================ */ +.page-frame { + background-color: var(--bg-frame); + border-left: 4px solid var(--accent); + padding: 6px 14px; + margin-bottom: 25px; + box-shadow: 0 3px 6px rgba(0,0,0,0.18); +} + +.page-frame h2 { + margin: 0; + line-height: 1.2; +} + +.section-title { + font-size: 1.2rem; + font-weight: 600; + color: var(--text-dark); +} + +.frame-row { + display: flex; + align-items: center; + gap: 12px; +} + +.frame-row h2 { + margin-right: auto; +} + +/* ================================ + Typography + ================================ */ +h1 { + margin-bottom: 20px; +} + +/* ================================ + Table styling + ================================ */ +table { + border-collapse: collapse; + width: 100%; + background-color: white; + box-shadow: 0 3px 6px rgba(0,0,0,0.18); +} + +th, td { + border: 1px solid var(--border-light); + padding: 10px 12px; + text-align: left; + vertical-align: middle; +} + +th { + background-color: #eee; + font-weight: bold; + cursor: pointer; + user-select: none; + position: relative; +} + +th:hover::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: var(--accent); +} + +.sort-arrow { + margin-left: 6px; + font-size: 12px; + opacity: 0.6; + display: inline-block; + width: 12px; /* prevents column shifting */ + text-align: center; +} + +/* ================================ + Action icons column + ================================ */ +td.actions { + white-space: nowrap; + text-align: left; +} + +.actions span { + cursor: pointer; + display: inline-flex; + align-items: center; + padding: 4px; + border-radius: 4px; + transition: background 0.2s ease; + margin-right: 8px; +} + +.actions span:hover { + background-color: #e0f0ff; +} + +/* ================================ + Logout button (pfSense style) + ================================ */ +.logout-btn { + background-color: var(--accent); + color: white; + border: none; + padding: 6px 14px; + font-size: 0.95rem; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.15); +} + +.logout-btn:hover { + background-color: var(--accent-hover); +} + +/* ================================ + Add-host button (pfSense style) + ================================ */ +.add-host-btn { + background-color: var(--accent); + color: white; + border: none; + padding: 6px 14px; + font-size: 0.95rem; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.15); +} + +.add-host-btn:hover { + background-color: var(--accent-hover); +} + +/* ================================ + Search bar + ================================ */ +.search-bar { + width: 250px; + padding: 8px 12px; + border: 1px solid var(--border-light); + border-radius: 6px; + font-size: 14px; +} + +/* ================================ + Toast notification + ================================ */ +.toast { + position: fixed; + top: 20px; + right: 20px; + background: #333; + color: white; + padding: 12px 18px; + border-radius: 6px; + opacity: 0; + pointer-events: none; + transition: opacity 0.4s ease; + font-size: 14px; + z-index: 9999; +} + +.toast.show { + opacity: 1; +} + +/* ================================ + Modal overlay + ================================ */ +.modal { + display: none; + position: fixed; + z-index: 2000; + inset: 0; + background: rgba(0,0,0,0.45); + justify-content: center; + align-items: center; +} + +/* ================================ + Modal window + ================================ */ +.modal-content { + background: #f4f4f4; + padding: 20px 24px; + border-left: 4px solid var(--accent); + border-radius: 6px; + width: 320px; + box-shadow: 0 4px 12px rgba(0,0,0,0.25); +} + +.modal-content h3 { + margin: 0 0 14px 0; + font-size: 1.2rem; + color: var(--text-dark); +} + +.modal-content label { + display: block; + margin-top: 10px; + font-weight: 600; + color: #333; +} + +.modal-content input[type="text"] { + width: 100%; + padding: 6px; + margin-top: 4px; + border: 1px solid #bbb; + border-radius: 4px; +} + +/* ================================ + Modal buttons + ================================ */ +.modal-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 18px; +} + +.cancel-btn { + background: #ccc; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; +} + +.save-btn { + background: var(--accent); + color: white; + border: none; + padding: 6px 14px; + border-radius: 4px; + cursor: pointer; +} + +.save-btn:hover { + background: var(--accent-hover); +} + +/* ================================ + SVG icons + ================================ */ +svg { + display: block; + pointer-events: none; +} \ No newline at end of file diff --git a/frontend/css/login.css b/frontend/css/login.css new file mode 100644 index 0000000..e1b2c78 --- /dev/null +++ b/frontend/css/login.css @@ -0,0 +1,76 @@ +body { + margin: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #f3f6fb; +} + +.login-wrapper { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; +} + +.login-box { + background: #ffffff; + padding: 24px 28px; + border-radius: 8px; + box-shadow: 0 4px 14px rgba(0,0,0,0.12); + width: 320px; +} + +.login-logo { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + color: #4da3ff; + font-weight: 600; +} + +.login-box h2 { + margin: 0 0 16px; + font-size: 1.2rem; +} + +label { + display: block; + font-size: 0.85rem; + margin-bottom: 4px; + color: #444; +} + +input[type="text"], +input[type="password"] { + width: 100%; + padding: 7px 10px; + border-radius: 4px; + border: 1px solid #ccc; + font-size: 0.95rem; + margin-bottom: 12px; + box-sizing: border-box; +} + +.login-btn { + width: 100%; + background-color: #4da3ff; + color: white; + border: none; + padding: 8px 0; + font-size: 0.95rem; + font-weight: 600; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; +} + +.login-btn:hover { + background-color: #1f8bff; +} + +.login-error { + margin-top: 10px; + font-size: 0.85rem; + color: #d9534f; + min-height: 16px; +} \ No newline at end of file diff --git a/frontend/hosts.html b/frontend/hosts.html new file mode 100644 index 0000000..b74b830 --- /dev/null +++ b/frontend/hosts.html @@ -0,0 +1,97 @@ + + + + + Network Manager + + + + +
+
+ + + +
+
+ +
+ +
+
+

🖧 Host List

+ + + + + +
+
+ + + + + + + + + + + + + + +
Name IPv4 IPv6 MAC Note SSL Actions
+ + + + + + + + diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index dcc2e97..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - Network Manager - - - - -
- -
- -
- -
-
-

🖧 Host List

- - - - - -
-
- - - - - - - - - - - - - - -
Name IPv4 IPv6 MAC Note SSL Actions
- - - - - - - - diff --git a/frontend/login.html b/frontend/login.html new file mode 100644 index 0000000..4f91f95 --- /dev/null +++ b/frontend/login.html @@ -0,0 +1,43 @@ + + + + + Network Manager - Login + + + + +
+ +
+ + + + + \ No newline at end of file diff --git a/frontend/style.css b/frontend/style.css deleted file mode 100644 index e4c72d4..0000000 --- a/frontend/style.css +++ /dev/null @@ -1,302 +0,0 @@ -/* ================================ - Global color variables (pfSense style) - ================================ */ -:root { - --accent: #4da3ff; - --accent-hover: #1f8bff; - - --bg-dark: #111; - --bg-light: #f9f9f9; - --bg-frame: #e8e8e8; - - --text-light: #e6e6e6; - --text-dark: #222; - - --border-light: #ccc; -} - -/* ================================ - Global layout - ================================ */ -body { - font-family: sans-serif; - margin: 20px; - padding-top: 60px; /* space for fixed topbar */ - background-color: var(--bg-light); -} - -/* ================================ - Topbar (pfSense full-width header) - ================================ */ -.topbar { - width: 100%; - background: var(--bg-dark); - padding: 14px 26px; - display: flex; - align-items: center; - border-bottom: 3px solid var(--accent); - position: fixed; - top: 0; - left: 0; - z-index: 1000; -} - -.logo { - display: flex; - align-items: center; - gap: 14px; -} - -.logo span { - font-size: 1.6rem; - font-weight: 600; - color: var(--text-light); - letter-spacing: 0.5px; -} - -.logo svg { - filter: drop-shadow(0 0 2px rgba(0,0,0,0.6)); -} - -/* ================================ - Page frame (section header) - ================================ */ -.page-frame { - background-color: var(--bg-frame); - border-left: 4px solid var(--accent); - padding: 6px 14px; - margin-bottom: 25px; - box-shadow: 0 3px 6px rgba(0,0,0,0.18); -} - -.page-frame h2 { - margin: 0; - line-height: 1.2; -} - -.section-title { - font-size: 1.2rem; - font-weight: 600; - color: var(--text-dark); -} - -.frame-row { - display: flex; - align-items: center; - gap: 12px; -} - -.frame-row h2 { - margin-right: auto; -} - -/* ================================ - Typography - ================================ */ -h1 { - margin-bottom: 20px; -} - -/* ================================ - Table styling - ================================ */ -table { - border-collapse: collapse; - width: 100%; - background-color: white; - box-shadow: 0 3px 6px rgba(0,0,0,0.18); -} - -th, td { - border: 1px solid var(--border-light); - padding: 10px 12px; - text-align: left; - vertical-align: middle; -} - -th { - background-color: #eee; - font-weight: bold; - cursor: pointer; - user-select: none; - position: relative; -} - -th:hover::after { - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 2px; - background: var(--accent); -} - -.sort-arrow { - margin-left: 6px; - font-size: 12px; - opacity: 0.6; - display: inline-block; - width: 12px; /* prevents column shifting */ - text-align: center; -} - -/* ================================ - Action icons column - ================================ */ -td.actions { - white-space: nowrap; - text-align: left; -} - -.actions span { - cursor: pointer; - display: inline-flex; - align-items: center; - padding: 4px; - border-radius: 4px; - transition: background 0.2s ease; - margin-right: 8px; -} - -.actions span:hover { - background-color: #e0f0ff; -} - -/* ================================ - Add-host button (pfSense style) - ================================ */ -.add-host-btn { - background-color: var(--accent); - color: white; - border: none; - padding: 6px 14px; - font-size: 0.95rem; - font-weight: 600; - border-radius: 4px; - cursor: pointer; - transition: background 0.2s ease; - box-shadow: 0 2px 4px rgba(0,0,0,0.15); -} - -.add-host-btn:hover { - background-color: var(--accent-hover); -} - -/* ================================ - Search bar - ================================ */ -.search-bar { - width: 250px; - padding: 8px 12px; - border: 1px solid var(--border-light); - border-radius: 6px; - font-size: 14px; -} - -/* ================================ - Toast notification - ================================ */ -.toast { - position: fixed; - top: 20px; - right: 20px; - background: #333; - color: white; - padding: 12px 18px; - border-radius: 6px; - opacity: 0; - pointer-events: none; - transition: opacity 0.4s ease; - font-size: 14px; - z-index: 9999; -} - -.toast.show { - opacity: 1; -} - -/* ================================ - Modal overlay - ================================ */ -.modal { - display: none; - position: fixed; - z-index: 2000; - inset: 0; - background: rgba(0,0,0,0.45); - justify-content: center; - align-items: center; -} - -/* ================================ - Modal window - ================================ */ -.modal-content { - background: #f4f4f4; - padding: 20px 24px; - border-left: 4px solid var(--accent); - border-radius: 6px; - width: 320px; - box-shadow: 0 4px 12px rgba(0,0,0,0.25); -} - -.modal-content h3 { - margin: 0 0 14px 0; - font-size: 1.2rem; - color: var(--text-dark); -} - -.modal-content label { - display: block; - margin-top: 10px; - font-weight: 600; - color: #333; -} - -.modal-content input[type="text"] { - width: 100%; - padding: 6px; - margin-top: 4px; - border: 1px solid #bbb; - border-radius: 4px; -} - -/* ================================ - Modal buttons - ================================ */ -.modal-buttons { - display: flex; - justify-content: flex-end; - gap: 10px; - margin-top: 18px; -} - -.cancel-btn { - background: #ccc; - border: none; - padding: 6px 12px; - border-radius: 4px; - cursor: pointer; -} - -.save-btn { - background: var(--accent); - color: white; - border: none; - padding: 6px 14px; - border-radius: 4px; - cursor: pointer; -} - -.save-btn:hover { - background: var(--accent-hover); -} - -/* ================================ - SVG icons - ================================ */ -svg { - display: block; - pointer-events: none; -} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f0c88a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi +uvicorn[standard] +itsdangerous \ No newline at end of file