From 437bb8211d148b919441bb2ba683ed9a5b89da13 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Sat, 3 Jan 2026 22:28:00 +0100 Subject: [PATCH] Added login (to be compleated now only with static user) --- Dockerfile | 5 +- backend/main.py | 110 ++++++++++++++++++++------ frontend/app.js | 49 +++++++++++- frontend/{style.css => css/hosts.css} | 32 +++++++- frontend/css/login.css | 76 ++++++++++++++++++ frontend/{index.html => hosts.html} | 30 ++++--- frontend/login.html | 43 ++++++++++ requirements.txt | 3 + 8 files changed, 306 insertions(+), 42 deletions(-) rename frontend/{style.css => css/hosts.css} (90%) create mode 100644 frontend/css/login.css rename frontend/{index.html => hosts.html} (75%) create mode 100644 frontend/login.html create mode 100644 requirements.txt 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/style.css b/frontend/css/hosts.css similarity index 90% rename from frontend/style.css rename to frontend/css/hosts.css index e4c72d4..b36e0df 100644 --- a/frontend/style.css +++ b/frontend/css/hosts.css @@ -31,7 +31,7 @@ body { .topbar { width: 100%; background: var(--bg-dark); - padding: 14px 26px; + padding: 0; display: flex; align-items: center; border-bottom: 3px solid var(--accent); @@ -41,6 +41,16 @@ body { 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; @@ -163,6 +173,26 @@ td.actions { 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) ================================ */ 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/index.html b/frontend/hosts.html similarity index 75% rename from frontend/index.html rename to frontend/hosts.html index dcc2e97..b74b830 100644 --- a/frontend/index.html +++ b/frontend/hosts.html @@ -3,21 +3,25 @@ Network Manager - +
-
@@ -57,8 +61,6 @@ - - + + 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/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 -- 2.47.3