diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3559644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +.env + +# Node +node_modules/ +dist/ +.DS_Store + +# Data persistante — SQLite & secrets (ne pas committer) +vps-monitor/data/*.db +vps-monitor/data/.jwt_secret + +# Anciens fichiers JSON de données +vps-monitor/backend/data/ diff --git a/vps-monitor/.pids b/vps-monitor/.pids index dbff357..f21dac2 100644 --- a/vps-monitor/.pids +++ b/vps-monitor/.pids @@ -1,2 +1,2 @@ -97732 -97787 +1456 +1540 diff --git a/vps-monitor/backend/__pycache__/main.cpython-313.pyc b/vps-monitor/backend/__pycache__/main.cpython-313.pyc index 076327c..9bf1e4b 100644 Binary files a/vps-monitor/backend/__pycache__/main.cpython-313.pyc and b/vps-monitor/backend/__pycache__/main.cpython-313.pyc differ diff --git a/vps-monitor/backend/data/vps.json b/vps-monitor/backend/data/vps.json index 0637a08..c2f6c65 100644 --- a/vps-monitor/backend/data/vps.json +++ b/vps-monitor/backend/data/vps.json @@ -1 +1,10 @@ -[] \ No newline at end of file +[ + { + "id": "test-1", + "name": "test", + "host": "45.145.167.66", + "port": 8001, + "api_key": "1fbbcb4998f99ab74c88e5076355e99531a76eb7c2e34b82318b7c334900a848", + "description": "" + } +] \ No newline at end of file diff --git a/vps-monitor/backend/main.py b/vps-monitor/backend/main.py index 191e8fe..8b29a70 100644 --- a/vps-monitor/backend/main.py +++ b/vps-monitor/backend/main.py @@ -5,9 +5,10 @@ Agrège les données de tous les agents et expose une API REST pour le frontend. """ import asyncio -import json import os import secrets +import sqlite3 +from contextlib import contextmanager from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Annotated @@ -22,18 +23,13 @@ from pydantic import BaseModel # ─── Config ─────────────────────────────────────────────────────────────────── -CONFIG_FILE = Path(os.getenv("CONFIG_FILE", "data/vps.json")) -USERS_FILE = Path(os.getenv("USERS_FILE", "data/users.json")) +DB_FILE = Path(os.getenv("DB_FILE", "data/monitor.db")) SECRET_FILE = Path(os.getenv("SECRET_FILE", "data/.jwt_secret")) AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "5")) JWT_ALGORITHM = "HS256" JWT_EXPIRE_MIN = int(os.getenv("JWT_EXPIRE_MINUTES", "1440")) # 24 h -CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) -if not CONFIG_FILE.exists(): - CONFIG_FILE.write_text("[]") -if not USERS_FILE.exists(): - USERS_FILE.write_text("[]") +DB_FILE.parent.mkdir(parents=True, exist_ok=True) # Clé JWT : env var > fichier persisté > génération + sauvegarde def _load_jwt_secret() -> str: @@ -76,22 +72,78 @@ class LoginRequest(BaseModel): password: str +# ─── SQLite ─────────────────────────────────────────────────────────────────── + +@contextmanager +def get_db(): + conn = sqlite3.connect(DB_FILE, check_same_thread=False) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + except Exception: + conn.rollback() + raise + finally: + conn.close() + + +def init_db() -> None: + with get_db() as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS users ( + username TEXT PRIMARY KEY, + password TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'admin' + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS vps ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + host TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 8001, + api_key TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '' + ) + """) + + +init_db() + + # ─── Persistance ────────────────────────────────────────────────────────────── -def load_vps() -> list[dict]: - return json.loads(CONFIG_FILE.read_text()) - - -def save_vps(data: list[dict]) -> None: - CONFIG_FILE.write_text(json.dumps(data, indent=2)) - - def load_users() -> list[dict]: - return json.loads(USERS_FILE.read_text()) + with get_db() as conn: + return [dict(r) for r in conn.execute("SELECT * FROM users").fetchall()] -def save_users(data: list[dict]) -> None: - USERS_FILE.write_text(json.dumps(data, indent=2)) +def add_user(user: dict) -> None: + with get_db() as conn: + conn.execute( + "INSERT INTO users (username, password, role) VALUES (?, ?, ?)", + (user["username"], user["password"], user["role"]), + ) + + +def load_vps() -> list[dict]: + with get_db() as conn: + return [dict(r) for r in conn.execute("SELECT * FROM vps").fetchall()] + + +def insert_vps(vps: dict) -> None: + with get_db() as conn: + conn.execute( + "INSERT INTO vps (id, name, host, port, api_key, description) VALUES (?, ?, ?, ?, ?, ?)", + (vps["id"], vps["name"], vps["host"], vps["port"], vps["api_key"], vps.get("description", "")), + ) + + +def remove_vps(vps_id: str) -> bool: + with get_db() as conn: + cur = conn.execute("DELETE FROM vps WHERE id = ?", (vps_id,)) + return cur.rowcount > 0 # ─── Auth helpers ───────────────────────────────────────────────────────────── @@ -189,8 +241,7 @@ def auth_status(): @app.post("/api/auth/register", status_code=201) def register(body: RegisterRequest): """Enregistre le premier utilisateur (admin). Fermé ensuite.""" - users = load_users() - if len(users) > 0: + if len(load_users()) > 0: raise HTTPException( status_code=403, detail="L'enregistrement public est désactivé. Seul l'admin peut créer des comptes." @@ -202,8 +253,7 @@ def register(body: RegisterRequest): "password": _bcrypt.hashpw(body.password.encode(), _bcrypt.gensalt()).decode(), "role": "admin", } - users.append(user) - save_users(users) + add_user(user) token = create_token(user["username"], user["role"]) return {"access_token": token, "token_type": "bearer", "role": user["role"]} @@ -238,22 +288,17 @@ def list_vps(_: Annotated[dict, Depends(get_current_user)]): @app.post("/api/vps", status_code=201) def add_vps(vps: VpsConfig, _: Annotated[dict, Depends(get_current_user)]): """Ajoute un nouveau VPS.""" - data = load_vps() - if any(v["id"] == vps.id for v in data): + if any(v["id"] == vps.id for v in load_vps()): raise HTTPException(status_code=409, detail="Un VPS avec cet ID existe déjà") - data.append(vps.model_dump()) - save_vps(data) + insert_vps(vps.model_dump()) return {"status": "ok", "id": vps.id} @app.delete("/api/vps/{vps_id}") def delete_vps(vps_id: str, _: Annotated[dict, Depends(get_current_user)]): """Supprime un VPS de la configuration.""" - data = load_vps() - filtered = [v for v in data if v["id"] != vps_id] - if len(filtered) == len(data): + if not remove_vps(vps_id): raise HTTPException(status_code=404, detail="VPS introuvable") - save_vps(filtered) return {"status": "ok"} diff --git a/vps-monitor/data/.gitkeep b/vps-monitor/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vps-monitor/docker-compose.yml b/vps-monitor/docker-compose.yml index 7bf1ea1..f65a791 100644 --- a/vps-monitor/docker-compose.yml +++ b/vps-monitor/docker-compose.yml @@ -4,7 +4,7 @@ services: ports: - "8000:8000" volumes: - - ./backend/data:/app/data + - ./data:/app/data env_file: ./backend/.env restart: unless-stopped