]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added login (to be compleated now only with static user)
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 3 Jan 2026 21:28:00 +0000 (22:28 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Sat, 3 Jan 2026 21:28:00 +0000 (22:28 +0100)
Dockerfile
backend/main.py
frontend/app.js
frontend/css/hosts.css [new file with mode: 0644]
frontend/css/login.css [new file with mode: 0644]
frontend/hosts.html [new file with mode: 0644]
frontend/index.html [deleted file]
frontend/login.html [new file with mode: 0644]
frontend/style.css [deleted file]
requirements.txt [new file with mode: 0644]

index ca092c910648f93c5aebdfc26e0301ecec8f3e8e..83a4e1bc3f34ea9b29f351f3bcf3b15d29828cb7 100644 (file)
@@ -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/
index 498653fd9f6e0bbe4fed975550a0b3b36a0e0271..21e892ed7bf55be28f0721623e7fafa4d56d1b88 100644 (file)
@@ -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"}
index 89bfa034d8c8da06de3a3ca1b05ed05b38bb18de..8d33d86b67dac6fe794621268c194042ea20b63b 100644 (file)
@@ -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 (file)
index 0000000..b36e0df
--- /dev/null
@@ -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 (file)
index 0000000..e1b2c78
--- /dev/null
@@ -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 (file)
index 0000000..b74b830
--- /dev/null
@@ -0,0 +1,97 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>Network Manager</title>
+    <link rel="stylesheet" href="css/hosts.css">
+</head>
+<body>
+
+<header class="topbar">
+    <div class="topbar-inner">
+        <div class="logo">
+            <svg width="30" height="30" viewBox="0 0 24 24" fill="#4da3ff">
+                <circle cx="12" cy="4" r="2"/>
+                <circle cx="4" cy="12" r="2"/>
+                <circle cx="20" cy="12" r="2"/>
+                <circle cx="12" cy="20" r="2"/>
+                <line x1="12" y1="6" x2="12" y2="18" stroke="#4da3ff" stroke-width="2"/>
+                <line x1="6" y1="12" x2="18" y2="12" stroke="#4da3ff" stroke-width="2"/>
+            </svg>
+            <span>Network Manager</span>
+        </div>
+
+        <button id="logoutBtn" class="logout-btn">Logout</button>
+    </div>
+</header>
+
+<div id="toast" class="toast"></div>
+
+<section class="page-frame">
+    <div class="frame-row">
+        <h2><span class="section-title">🖧 Host List</span></h2>
+       
+        <input 
+            type="text" 
+            id="searchInput" 
+            placeholder="Ricerca..." 
+            oninput="filterHosts()" 
+            class="search-bar"
+        />
+
+        <button class="add-host-btn" onclick="openAddHostModal()">
+            + Add Host
+        </button>
+
+    </div>
+</section>
+
+<table id="hosts-table">
+    <thead>
+        <tr>
+            <th onclick="sortTable(0)">Name <span class="sort-arrow"></span></th>
+            <th onclick="sortTable(1)">IPv4 <span class="sort-arrow"></span></th>
+            <th onclick="sortTable(2)">IPv6 <span class="sort-arrow"></span></th>
+            <th onclick="sortTable(3)">MAC <span class="sort-arrow"></span></th>
+            <th onclick="sortTable(4)">Note <span class="sort-arrow"></span></th>
+            <th onclick="sortTable(5)">SSL <span class="sort-arrow"></span></th>
+            <th>Actions</th>
+        </tr>
+    </thead>
+    <tbody></tbody>
+</table>
+
+<!-- Popup Add Host -->
+<div id="addHostModal" class="modal">
+    <div class="modal-content">
+        <h3>Aggiungi Host</h3>
+
+        <label>Nome</label>
+        <input type="text" id="hostName">
+
+        <label>IPv4</label>
+        <input type="text" id="hostIPv4">
+
+        <label>IPv6</label>
+        <input type="text" id="hostIPv6">
+
+        <label>MAC Address</label>
+        <input type="text" id="hostMAC">
+
+        <label>Note</label>
+        <input type="text" id="hostNote">
+
+        <label>SSL?</label>
+        <input type="checkbox" id="hostSSL">
+
+        <div class="modal-buttons">
+            <button class="cancel-btn" onclick="closeAddHostModal()">Annulla</button>
+            <button class="save-btn" onclick="saveHost()">Salva</button>
+        </div>
+    </div>
+</div>
+
+<script src="app.js"></script>
+
+</body>
+</html>
diff --git a/frontend/index.html b/frontend/index.html
deleted file mode 100644 (file)
index dcc2e97..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="UTF-8">
-    <title>Network Manager</title>
-    <link rel="stylesheet" href="style.css">
-</head>
-<body>
-
-<header class="topbar">
-    <div class="logo">
-        <svg width="30" height="30" viewBox="0 0 24 24" fill="#4da3ff">
-            <circle cx="12" cy="4" r="2"/>
-            <circle cx="4" cy="12" r="2"/>
-            <circle cx="20" cy="12" r="2"/>
-            <circle cx="12" cy="20" r="2"/>
-            <line x1="12" y1="6" x2="12" y2="18" stroke="#4da3ff" stroke-width="2"/>
-            <line x1="6" y1="12" x2="18" y2="12" stroke="#4da3ff" stroke-width="2"/>
-        </svg>
-        <span>Network Manager</span>
-    </div>
-</header>
-
-<div id="toast" class="toast"></div>
-
-<section class="page-frame">
-    <div class="frame-row">
-        <h2><span class="section-title">🖧 Host List</span></h2>
-       
-        <input 
-            type="text" 
-            id="searchInput" 
-            placeholder="Ricerca..." 
-            oninput="filterHosts()" 
-            class="search-bar"
-        />
-
-        <button class="add-host-btn" onclick="openAddHostModal()">
-            + Add Host
-        </button>
-
-    </div>
-</section>
-
-<table id="hosts-table">
-    <thead>
-        <tr>
-            <th onclick="sortTable(0)">Name <span class="sort-arrow"></span></th>
-            <th onclick="sortTable(1)">IPv4 <span class="sort-arrow"></span></th>
-            <th onclick="sortTable(2)">IPv6 <span class="sort-arrow"></span></th>
-            <th onclick="sortTable(3)">MAC <span class="sort-arrow"></span></th>
-            <th onclick="sortTable(4)">Note <span class="sort-arrow"></span></th>
-            <th onclick="sortTable(5)">SSL <span class="sort-arrow"></span></th>
-            <th>Actions</th>
-        </tr>
-    </thead>
-    <tbody></tbody>
-</table>
-
-<script src="app.js"></script>
-
-<!-- Popup Add Host -->
-<div id="addHostModal" class="modal">
-    <div class="modal-content">
-        <h3>Aggiungi Host</h3>
-
-        <label>Nome</label>
-        <input type="text" id="hostName">
-
-        <label>IPv4</label>
-        <input type="text" id="hostIPv4">
-
-        <label>IPv6</label>
-        <input type="text" id="hostIPv6">
-
-        <label>MAC Address</label>
-        <input type="text" id="hostMAC">
-
-        <label>Note</label>
-        <input type="text" id="hostNote">
-
-        <label>SSL?</label>
-        <input type="checkbox" id="hostSSL">
-
-        <div class="modal-buttons">
-            <button class="cancel-btn" onclick="closeAddHostModal()">Annulla</button>
-            <button class="save-btn" onclick="saveHost()">Salva</button>
-        </div>
-    </div>
-</div>
-
-</body>
-</html>
diff --git a/frontend/login.html b/frontend/login.html
new file mode 100644 (file)
index 0000000..4f91f95
--- /dev/null
@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="UTF-8">
+    <title>Network Manager - Login</title>
+    <link rel="stylesheet" href="css/login.css">
+</head>
+<body>
+
+<div class="login-wrapper">
+    <div class="login-box">
+        <div class="login-logo">
+            <svg width="34" height="34" viewBox="0 0 24 24" fill="#4da3ff">
+                <circle cx="12" cy="4" r="2"/>
+                <circle cx="4" cy="12" r="2"/>
+                <circle cx="20" cy="12" r="2"/>
+                <circle cx="12" cy="20" r="2"/>
+                <line x1="12" y1="6" x2="12" y2="18" stroke="#4da3ff" stroke-width="2"/>
+                <line x1="6" y1="12" x2="18" y2="12" stroke="#4da3ff" stroke-width="2"/>
+            </svg>
+            <span>Network Manager</span>
+        </div>
+
+        <h2>Login</h2>
+
+        <form id="loginForm" onsubmit="return handleLogin(event)">
+            <label for="username">Username</label>
+            <input type="text" id="username" autocomplete="off" required>
+
+            <label for="password">Password</label>
+            <input type="password" id="password" required>
+
+            <button type="submit" class="login-btn">Entra</button>
+        </form>
+
+        <div id="loginError" class="login-error"></div>
+    </div>
+</div>
+
+<script src="app.js"></script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/frontend/style.css b/frontend/style.css
deleted file mode 100644 (file)
index e4c72d4..0000000
+++ /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 (file)
index 0000000..f0c88a6
--- /dev/null
@@ -0,0 +1,3 @@
+fastapi 
+uvicorn[standard]
+itsdangerous
\ No newline at end of file