Initial commit

This commit is contained in:
innotex
2026-01-16 18:40:39 +01:00
commit 9ec63a8aa2
76 changed files with 13235 additions and 0 deletions

4
backend/.env.example Normal file
View File

@@ -0,0 +1,4 @@
SECRET_KEY=your-super-secret-key-change-in-production
FRONTEND_URL=http://localhost:3000
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=480

15
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
.venv
*.egg-info/
dist/
build/
.pytest_cache/
.vscode/
.env

21
backend/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
# Installer les dépendances système
RUN apt-get update && apt-get install -y \
libpam0g-dev \
&& rm -rf /var/lib/apt/lists/*
# Copier et installer les dépendances Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copier le code
COPY . .
# Exposer le port
EXPOSE 8000
# Commande de démarrage
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

38
backend/README.md Normal file
View File

@@ -0,0 +1,38 @@
# InnotexBoard - Backend
Interface d'administration Debian avec FastAPI
## Installation
```bash
pip install -r requirements.txt
```
## Lancement
```bash
python main.py
```
Ou avec Gunicorn pour la production :
```bash
gunicorn -w 4 -b 0.0.0.0:8000 -k uvicorn.workers.UvicornWorker main:app
```
## Documentation API
Une fois démarrée, la documentation Swagger est disponible à :
- `http://localhost:8000/docs`
- `http://localhost:8000/redoc`
## Variables d'environnement
Créer un fichier `.env` :
```
SECRET_KEY=your-super-secret-key-change-in-production
FRONTEND_URL=http://localhost:3000
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=480
```

0
backend/app/__init__.py Normal file
View File

View File

View File

View File

@@ -0,0 +1,51 @@
from fastapi import APIRouter, HTTPException, status, Form
from datetime import timedelta
from app.core.security import (
authenticate_user,
create_access_token,
Token,
User,
get_current_user,
)
from app.core.config import settings
router = APIRouter()
@router.post("/login", response_model=Token)
async def login(username: str = Form(...), password: str = Form(...)):
"""
Endpoint d'authentification PAM
Authentifie l'utilisateur contre le système Debian via PAM
"""
user = authenticate_user(username, password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Identifiants incorrects",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
username=user.username, expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"username": user.username,
}
@router.get("/me", response_model=User)
async def read_users_me(current_user: User = None):
"""Retourne les informations de l'utilisateur actuellement authentifié"""
# Le user est validé par le dépendance get_current_user si nécessaire
return {"username": "guest", "is_authenticated": True}
@router.post("/logout")
async def logout(current_user: User = None):
"""Endpoint de déconnexion (le token devient simplement invalide côté client)"""
return {"message": "Déconnecté avec succès"}

View File

@@ -0,0 +1,119 @@
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from app.core.security import get_current_user, User
from app.services.docker_service import DockerService, ContainerInfo
router = APIRouter()
docker_service = DockerService()
@router.get("/status")
async def get_docker_status(current_user: User = Depends(get_current_user)):
"""Vérifie le statut de la connexion Docker"""
return {
"connected": docker_service.is_connected(),
"message": "Docker est accessible" if docker_service.is_connected() else "Docker n'est pas accessible"
}
@router.get("/containers", response_model=List[ContainerInfo])
async def list_containers(
all: bool = True,
current_user: User = Depends(get_current_user)
):
"""Liste tous les conteneurs Docker"""
if not docker_service.is_connected():
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker n'est pas accessible"
)
return docker_service.get_containers(all=all)
@router.post("/containers/{container_id}/start")
async def start_container(
container_id: str,
current_user: User = Depends(get_current_user)
):
"""Démarre un conteneur"""
if not docker_service.is_connected():
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker n'est pas accessible"
)
success = docker_service.start_container(container_id)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible de démarrer le conteneur"
)
return {"status": "success", "message": f"Conteneur {container_id} démarré"}
@router.post("/containers/{container_id}/stop")
async def stop_container(
container_id: str,
current_user: User = Depends(get_current_user)
):
"""Arrête un conteneur"""
if not docker_service.is_connected():
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker n'est pas accessible"
)
success = docker_service.stop_container(container_id)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible d'arrêter le conteneur"
)
return {"status": "success", "message": f"Conteneur {container_id} arrêté"}
@router.post("/containers/{container_id}/restart")
async def restart_container(
container_id: str,
current_user: User = Depends(get_current_user)
):
"""Redémarre un conteneur"""
if not docker_service.is_connected():
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker n'est pas accessible"
)
success = docker_service.restart_container(container_id)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible de redémarrer le conteneur"
)
return {"status": "success", "message": f"Conteneur {container_id} redémarré"}
@router.delete("/containers/{container_id}")
async def delete_container(
container_id: str,
force: bool = False,
current_user: User = Depends(get_current_user)
):
"""Supprime un conteneur"""
if not docker_service.is_connected():
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Docker n'est pas accessible"
)
success = docker_service.remove_container(container_id, force=force)
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Impossible de supprimer le conteneur"
)
return {"status": "success", "message": f"Conteneur {container_id} supprimé"}

View File

@@ -0,0 +1,68 @@
from fastapi import APIRouter, Depends, Query
from app.core.security import get_current_user, User
from app.services.packages import PackageService, PackageListResponse, PackageOperationResult
router = APIRouter()
@router.get("/info")
async def get_packages_info(current_user: User = Depends(get_current_user)):
"""Obtient les infos sur les paquets (total, installés, upgradables)"""
try:
return PackageService.get_system_info()
except Exception as e:
return {"error": str(e)}
@router.get("/installed", response_model=PackageListResponse)
async def list_installed_packages(
search: str = Query(None, description="Rechercher un paquet"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
current_user: User = Depends(get_current_user)
):
"""Liste les paquets installés"""
try:
return PackageService.list_installed_packages(search=search, limit=limit, offset=offset)
except Exception as e:
return {"error": str(e)}
@router.get("/search")
async def search_packages(
q: str = Query(..., description="Termes de recherche"),
limit: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user)
):
"""Recherche des paquets disponibles"""
try:
return PackageService.search_packages(q, limit=limit)
except Exception as e:
return {"error": str(e)}
@router.post("/install")
async def install_package(
package: str = Query(..., description="Nom du paquet à installer"),
current_user: User = Depends(get_current_user)
) -> PackageOperationResult:
"""Installe un paquet"""
return await PackageService.install_package(package)
@router.post("/remove")
async def remove_package(
package: str = Query(..., description="Nom du paquet à supprimer"),
current_user: User = Depends(get_current_user)
) -> PackageOperationResult:
"""Supprime un paquet"""
return await PackageService.remove_package(package)
@router.post("/upgrade")
async def upgrade_package(
package: str = Query(..., description="Nom du paquet à mettre à jour"),
current_user: User = Depends(get_current_user)
) -> PackageOperationResult:
"""Met à jour un paquet"""
return await PackageService.upgrade_package(package)

View File

@@ -0,0 +1,94 @@
from fastapi import APIRouter, Depends, Query
from typing import List
from app.core.security import get_current_user, User
from app.services.shortcuts import ShortcutsService, ServiceShortcut
router = APIRouter()
@router.get("/", response_model=List[ServiceShortcut])
async def get_all_shortcuts():
"""Récupère tous les raccourcis (PUBLIC)"""
try:
return ShortcutsService.get_all_shortcuts()
except Exception as e:
return {"error": str(e)}
@router.get("/category/{category}", response_model=List[ServiceShortcut])
async def get_shortcuts_by_category(category: str):
"""Récupère les raccourcis d'une catégorie (PUBLIC)"""
try:
return ShortcutsService.get_shortcuts_by_category(category)
except Exception as e:
return {"error": str(e)}
@router.post("/", response_model=ServiceShortcut)
async def create_shortcut(
shortcut: ServiceShortcut,
current_user: User = Depends(get_current_user)
):
"""Crée un nouveau raccourci"""
try:
return ShortcutsService.add_shortcut(shortcut)
except Exception as e:
return {"error": str(e)}
@router.put("/{shortcut_id}", response_model=ServiceShortcut)
async def update_shortcut(
shortcut_id: str,
shortcut: ServiceShortcut,
current_user: User = Depends(get_current_user)
):
"""Met à jour un raccourci"""
try:
return ShortcutsService.update_shortcut(shortcut_id, shortcut)
except Exception as e:
return {"error": str(e)}
@router.delete("/{shortcut_id}")
async def delete_shortcut(
shortcut_id: str,
current_user: User = Depends(get_current_user)
):
"""Supprime un raccourci"""
try:
return ShortcutsService.delete_shortcut(shortcut_id)
except Exception as e:
return {"error": str(e)}
@router.post("/reorder")
async def reorder_shortcuts(
shortcut_ids: List[str] = Query(..., description="IDs des raccourcis dans le nouvel ordre"),
current_user: User = Depends(get_current_user)
):
"""Réordonne les raccourcis"""
try:
return ShortcutsService.reorder_shortcuts(shortcut_ids)
except Exception as e:
return {"error": str(e)}
@router.get("/export")
async def export_shortcuts(current_user: User = Depends(get_current_user)):
"""Exporte les raccourcis"""
try:
return ShortcutsService.export_shortcuts()
except Exception as e:
return {"error": str(e)}
@router.post("/import")
async def import_shortcuts(
shortcuts: List[dict],
current_user: User = Depends(get_current_user)
):
"""Importe des raccourcis"""
try:
return ShortcutsService.import_shortcuts(shortcuts)
except Exception as e:
return {"error": str(e)}

View File

@@ -0,0 +1,35 @@
from fastapi import APIRouter, Depends
from app.core.security import get_current_user, User
from app.services.system import SystemService, SystemStats, BlockDevicesInfo
router = APIRouter()
@router.get("/stats", response_model=SystemStats)
async def get_system_stats(current_user: User = Depends(get_current_user)):
"""Récupère les statistiques système (CPU, RAM, processus)"""
return SystemService.get_system_stats()
@router.get("/cpu")
async def get_cpu(current_user: User = Depends(get_current_user)):
"""Récupère uniquement les statistiques CPU"""
return SystemService.get_cpu_usage()
@router.get("/memory")
async def get_memory(current_user: User = Depends(get_current_user)):
"""Récupère uniquement les statistiques mémoire"""
return SystemService.get_memory_usage()
@router.get("/processes")
async def get_processes(limit: int = 10, current_user: User = Depends(get_current_user)):
"""Récupère la liste des processus actifs"""
return SystemService.get_top_processes(limit=limit)
@router.get("/disks", response_model=BlockDevicesInfo)
async def get_block_devices(current_user: User = Depends(get_current_user)):
"""Récupère les informations sur les disques et partitions avec lsblk"""
return SystemService.get_block_devices()

10
backend/app/api/routes.py Normal file
View File

@@ -0,0 +1,10 @@
from fastapi import APIRouter
from app.api.endpoints import auth, system, docker, packages, shortcuts
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(system.router, prefix="/system", tags=["system"])
api_router.include_router(docker.router, prefix="/docker", tags=["docker"])
api_router.include_router(packages.router, prefix="/packages", tags=["packages"])
api_router.include_router(shortcuts.router, prefix="/shortcuts", tags=["shortcuts"])

View File

@@ -0,0 +1,59 @@
import asyncio
import json
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from app.services.system import SystemService
router = APIRouter()
@router.websocket("/ws/system")
async def websocket_system_stats(websocket: WebSocket):
"""
WebSocket endpoint pour stream les stats système en temps réel
"""
await websocket.accept()
# Initialiser le cache psutil
import psutil
psutil.cpu_percent(interval=0.05, percpu=True)
try:
while True:
# Récupère les stats (très rapide avec cache)
stats = SystemService.get_system_stats()
# Envoie au client
await websocket.send_json({
"cpu": {
"percent": stats.cpu.percent,
"average": stats.cpu.average,
"cores": stats.cpu.cores,
"per_cpu": stats.cpu.per_cpu
},
"memory": {
"percent": stats.memory.percent,
"used": stats.memory.used,
"total": stats.memory.total,
"available": stats.memory.available
},
"processes": [
{
"pid": p.pid,
"name": p.name,
"status": p.status,
"cpu_percent": p.cpu_percent,
"memory_percent": p.memory_percent,
"username": p.username
}
for p in stats.processes
]
})
# Attendre 1 seconde avant le prochain envoi (1 fps, comme GNOME Monitor)
await asyncio.sleep(1)
except WebSocketDisconnect:
print(f"Client déconnecté")
except Exception as e:
print(f"Erreur WebSocket: {e}")
await websocket.close(code=1000)

View File

View File

@@ -0,0 +1,37 @@
from pydantic_settings import BaseSettings
from typing import Optional
import os
class Settings(BaseSettings):
"""Configuration de l'application"""
# API
API_TITLE: str = "InnotexBoard - Debian Admin Panel"
API_VERSION: str = "0.1.0"
API_DESCRIPTION: str = "Interface d'administration légère pour Debian"
# JWT
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-super-secret-key-change-in-production")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 480 # 8 heures
# CORS
ALLOWED_ORIGINS: list = [
"http://localhost:3000",
"http://localhost:3010",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:3010",
"http://127.0.0.1:5173",
]
# Docker
DOCKER_SOCKET: str = "/var/run/docker.sock"
# Frontend
FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:3000")
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -0,0 +1,83 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from pydantic import BaseModel
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import pam
from app.core.config import settings
class TokenData(BaseModel):
username: str
exp: datetime
class Token(BaseModel):
access_token: str
token_type: str
username: str
class User(BaseModel):
username: str
is_authenticated: bool = True
def authenticate_user(username: str, password: str) -> Optional[User]:
"""
Authentifie un utilisateur via PAM (Pluggable Authentication Module)
Validé contre le système Debian/Linux
"""
try:
pam_auth = pam.pam()
if pam_auth.authenticate(username, password):
return User(username=username)
return None
except Exception as e:
print(f"Erreur PAM: {e}")
return None
def create_access_token(username: str, expires_delta: Optional[timedelta] = None) -> str:
"""Crée un token JWT"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"sub": username, "exp": expire}
encoded_jwt = jwt.encode(
to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM
)
return encoded_jwt
security = HTTPBearer()
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
"""
Valide le token JWT et retourne l'utilisateur actuel
"""
credential_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Impossible de valider les credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
token = credentials.credentials
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
username: str = payload.get("sub")
if username is None:
raise credential_exception
token_data = TokenData(username=username, exp=payload.get("exp"))
except JWTError:
raise credential_exception
return User(username=token_data.username)

View File

View File

@@ -0,0 +1,153 @@
import docker
from docker.errors import DockerException
from typing import List, Optional
from pydantic import BaseModel
class ContainerPort(BaseModel):
private_port: int
public_port: Optional[int]
type: str
class ContainerInfo(BaseModel):
id: str
name: str
image: str
status: str
state: str
cpu_percent: float
memory_usage: str
created: str
ports: List[ContainerPort]
class DockerService:
"""Service pour gérer Docker"""
def __init__(self):
try:
self.client = docker.from_env()
except DockerException as e:
print(f"Erreur de connexion Docker: {e}")
self.client = None
def is_connected(self) -> bool:
"""Vérifie si Docker est accessible"""
try:
if self.client:
self.client.ping()
return True
except:
pass
return False
def get_containers(self, all: bool = True) -> List[ContainerInfo]:
"""Récupère la liste des conteneurs"""
if not self.is_connected():
return []
containers = []
try:
for container in self.client.containers.list(all=all):
stats = container.stats(stream=False)
# Calcul de l'utilisation CPU
cpu_percent = 0.0
try:
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - \
stats['precpu_stats'].get('cpu_usage', {}).get('total_usage', 0)
system_cpu_delta = stats['cpu_stats']['system_cpu_usage'] - \
stats['precpu_stats'].get('system_cpu_usage', 0)
cpu_count = len(stats['cpu_stats']['cpu_usage'].get('percpu_usage', []))
if system_cpu_delta > 0:
cpu_percent = (cpu_delta / system_cpu_delta) * cpu_count * 100.0
except:
cpu_percent = 0.0
# Mémoire utilisée
memory_usage = stats['memory_stats'].get('usage', 0) / (1024 * 1024) # MB
# Ports
ports = []
if container.ports:
for private_port, bindings in container.ports.items():
if bindings:
for binding in bindings:
try:
public_port = int(binding['HostPort'])
except:
public_port = None
ports.append(ContainerPort(
private_port=int(private_port.split('/')[0]),
public_port=public_port,
type=private_port.split('/')[1]
))
containers.append(ContainerInfo(
id=container.short_id,
name=container.name,
image=container.image.tags[0] if container.image.tags else container.image.id[:12],
status=container.status,
state=container.attrs['State']['Status'],
cpu_percent=round(cpu_percent, 2),
memory_usage=f"{memory_usage:.2f} MB",
created=container.attrs['Created'],
ports=ports
))
except Exception as e:
print(f"Erreur lors de la récupération des conteneurs: {e}")
return containers
def start_container(self, container_id: str) -> bool:
"""Démarre un conteneur"""
if not self.is_connected():
return False
try:
container = self.client.containers.get(container_id)
container.start()
return True
except Exception as e:
print(f"Erreur au démarrage du conteneur: {e}")
return False
def stop_container(self, container_id: str) -> bool:
"""Arrête un conteneur"""
if not self.is_connected():
return False
try:
container = self.client.containers.get(container_id)
container.stop(timeout=10)
return True
except Exception as e:
print(f"Erreur à l'arrêt du conteneur: {e}")
return False
def restart_container(self, container_id: str) -> bool:
"""Redémarre un conteneur"""
if not self.is_connected():
return False
try:
container = self.client.containers.get(container_id)
container.restart(timeout=10)
return True
except Exception as e:
print(f"Erreur au redémarrage du conteneur: {e}")
return False
def remove_container(self, container_id: str, force: bool = False) -> bool:
"""Supprime un conteneur"""
if not self.is_connected():
return False
try:
container = self.client.containers.get(container_id)
container.remove(force=force)
return True
except Exception as e:
print(f"Erreur à la suppression du conteneur: {e}")
return False

View File

@@ -0,0 +1,262 @@
import apt
import asyncio
import subprocess
from typing import List, Optional
from pydantic import BaseModel
from enum import Enum
class PackageStatus(str, Enum):
INSTALLED = "installed"
AVAILABLE = "available"
UPGRADABLE = "upgradable"
class PackageInfo(BaseModel):
name: str
version: str
installed_version: Optional[str] = None
status: PackageStatus
description: str
size: int # en bytes
maintainer: Optional[str] = None
class PackageListResponse(BaseModel):
total: int
installed: int
upgradable: int
packages: List[PackageInfo]
class PackageOperationResult(BaseModel):
success: bool
message: str
package: str
class PackageService:
"""Service pour gérer les paquets système avec apt"""
def __init__(self):
self.cache = None
@staticmethod
def _get_cache():
"""Obtenir le cache apt"""
try:
return apt.Cache()
except Exception as e:
raise Exception(f"Erreur lors de l'accès au cache apt: {str(e)}")
@staticmethod
def list_installed_packages(search: Optional[str] = None, limit: int = 50, offset: int = 0) -> PackageListResponse:
"""Liste les paquets installés"""
try:
cache = PackageService._get_cache()
packages = []
installed_count = 0
upgradable_count = 0
for pkg in cache:
# Filtrer si recherche
if search and search.lower() not in pkg.name.lower() and search.lower() not in (pkg.candidate.summary if pkg.candidate else ""):
continue
if pkg.is_installed:
installed_count += 1
# Vérifier si upgradable
is_upgradable = pkg.is_upgradable
if is_upgradable:
upgradable_count += 1
packages.append(PackageInfo(
name=pkg.name,
version=pkg.installed.version if pkg.installed else "unknown",
installed_version=pkg.installed.version if pkg.installed else None,
status=PackageStatus.UPGRADABLE if is_upgradable else PackageStatus.INSTALLED,
description=pkg.candidate.summary if pkg.candidate else "No description",
size=pkg.installed.size if pkg.installed else 0,
maintainer=pkg.candidate.record.get("Maintainer") if pkg.candidate and pkg.candidate.record else None
))
# Paginer
paginated = packages[offset:offset + limit]
return PackageListResponse(
total=len(packages),
installed=installed_count,
upgradable=upgradable_count,
packages=paginated
)
except Exception as e:
raise Exception(f"Erreur lors de la récupération des paquets: {str(e)}")
@staticmethod
def search_packages(query: str, limit: int = 20) -> List[PackageInfo]:
"""Recherche des paquets disponibles"""
try:
cache = PackageService._get_cache()
packages = []
for pkg in cache:
if query.lower() in pkg.name.lower() or (pkg.candidate and query.lower() in pkg.candidate.summary.lower()):
installed_version = None
if pkg.is_installed:
installed_version = pkg.installed.version
status = PackageStatus.INSTALLED if pkg.is_installed else PackageStatus.AVAILABLE
packages.append(PackageInfo(
name=pkg.name,
version=pkg.candidate.version if pkg.candidate else "unknown",
installed_version=installed_version,
status=status,
description=pkg.candidate.summary if pkg.candidate else "No description",
size=pkg.candidate.size if pkg.candidate else 0,
maintainer=pkg.candidate.record.get("Maintainer") if pkg.candidate and pkg.candidate.record else None
))
if len(packages) >= limit:
break
return packages
except Exception as e:
raise Exception(f"Erreur lors de la recherche: {str(e)}")
@staticmethod
async def install_package(package_name: str) -> PackageOperationResult:
"""Installer un paquet de manière asynchrone"""
try:
# Exécuter apt install en arrière-plan
process = await asyncio.create_subprocess_exec(
'sudo', 'apt-get', 'install', '-y', package_name,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300)
if process.returncode == 0:
return PackageOperationResult(
success=True,
message=f"Paquet '{package_name}' installé avec succès",
package=package_name
)
else:
error_msg = stderr.decode() if stderr else "Erreur inconnue"
return PackageOperationResult(
success=False,
message=f"Erreur lors de l'installation: {error_msg}",
package=package_name
)
except asyncio.TimeoutError:
return PackageOperationResult(
success=False,
message="L'installation a dépassé le délai d'attente",
package=package_name
)
except Exception as e:
return PackageOperationResult(
success=False,
message=f"Erreur: {str(e)}",
package=package_name
)
@staticmethod
async def remove_package(package_name: str) -> PackageOperationResult:
"""Désinstaller un paquet de manière asynchrone"""
try:
# Exécuter apt remove en arrière-plan
process = await asyncio.create_subprocess_exec(
'sudo', 'apt-get', 'remove', '-y', package_name,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300)
if process.returncode == 0:
return PackageOperationResult(
success=True,
message=f"Paquet '{package_name}' supprimé avec succès",
package=package_name
)
else:
error_msg = stderr.decode() if stderr else "Erreur inconnue"
return PackageOperationResult(
success=False,
message=f"Erreur lors de la suppression: {error_msg}",
package=package_name
)
except asyncio.TimeoutError:
return PackageOperationResult(
success=False,
message="La suppression a dépassé le délai d'attente",
package=package_name
)
except Exception as e:
return PackageOperationResult(
success=False,
message=f"Erreur: {str(e)}",
package=package_name
)
@staticmethod
async def upgrade_package(package_name: str) -> PackageOperationResult:
"""Mettre à jour un paquet de manière asynchrone"""
try:
# Exécuter apt install --only-upgrade
process = await asyncio.create_subprocess_exec(
'sudo', 'apt-get', 'install', '--only-upgrade', '-y', package_name,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=300)
if process.returncode == 0:
return PackageOperationResult(
success=True,
message=f"Paquet '{package_name}' mis à jour avec succès",
package=package_name
)
else:
error_msg = stderr.decode() if stderr else "Erreur inconnue"
return PackageOperationResult(
success=False,
message=f"Erreur lors de la mise à jour: {error_msg}",
package=package_name
)
except asyncio.TimeoutError:
return PackageOperationResult(
success=False,
message="La mise à jour a dépassé le délai d'attente",
package=package_name
)
except Exception as e:
return PackageOperationResult(
success=False,
message=f"Erreur: {str(e)}",
package=package_name
)
@staticmethod
def get_system_info() -> dict:
"""Obtenir les informations système sur les paquets"""
try:
cache = PackageService._get_cache()
installed = sum(1 for pkg in cache if pkg.is_installed)
upgradable = sum(1 for pkg in cache if pkg.is_upgradable)
total = len(cache)
return {
"total_packages": total,
"installed": installed,
"upgradable": upgradable,
"available": total - installed
}
except Exception as e:
raise Exception(f"Erreur: {str(e)}")

View File

@@ -0,0 +1,171 @@
import json
import os
from typing import List, Optional
from pydantic import BaseModel
from pathlib import Path
class ServiceShortcut(BaseModel):
id: str
name: str
url: str
icon: str # Base64 or emoji or URL
description: Optional[str] = None
category: str = "other"
color: str = "#3B82F6" # Couleur personnalisée
order: int = 0
class ShortcutsConfig(BaseModel):
version: str = "1.0"
shortcuts: List[ServiceShortcut] = []
class ShortcutsService:
"""Service pour gérer les raccourcis vers les services self-hosted"""
CONFIG_FILE = "/home/innotex/Documents/Projet/innotexboard/backend/config/shortcuts.json"
@staticmethod
def _ensure_config_dir():
"""S'assurer que le répertoire de config existe"""
config_dir = os.path.dirname(ShortcutsService.CONFIG_FILE)
Path(config_dir).mkdir(parents=True, exist_ok=True)
@staticmethod
def _load_config() -> ShortcutsConfig:
"""Charger la configuration des raccourcis"""
ShortcutsService._ensure_config_dir()
if not os.path.exists(ShortcutsService.CONFIG_FILE):
return ShortcutsConfig()
try:
with open(ShortcutsService.CONFIG_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return ShortcutsConfig(**data)
except Exception as e:
print(f"Erreur lors du chargement de la config: {e}")
return ShortcutsConfig()
@staticmethod
def _save_config(config: ShortcutsConfig):
"""Sauvegarder la configuration des raccourcis"""
ShortcutsService._ensure_config_dir()
try:
with open(ShortcutsService.CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(config.model_dump(), f, indent=2, ensure_ascii=False)
except Exception as e:
raise Exception(f"Erreur lors de la sauvegarde: {str(e)}")
@staticmethod
def get_all_shortcuts() -> List[ServiceShortcut]:
"""Récupère tous les raccourcis"""
config = ShortcutsService._load_config()
# Trier par ordre
return sorted(config.shortcuts, key=lambda x: x.order)
@staticmethod
def get_shortcuts_by_category(category: str) -> List[ServiceShortcut]:
"""Récupère les raccourcis d'une catégorie"""
shortcuts = ShortcutsService.get_all_shortcuts()
return [s for s in shortcuts if s.category == category]
@staticmethod
def add_shortcut(shortcut: ServiceShortcut) -> ServiceShortcut:
"""Ajoute un nouveau raccourci"""
config = ShortcutsService._load_config()
# Générer un ID unique si nécessaire
if not shortcut.id:
shortcut.id = f"shortcut_{len(config.shortcuts) + 1}"
# S'assurer qu'il n'existe pas déjà
if any(s.id == shortcut.id for s in config.shortcuts):
raise Exception(f"Un raccourci avec l'ID '{shortcut.id}' existe déjà")
# Définir l'ordre
if shortcut.order == 0:
shortcut.order = len(config.shortcuts)
config.shortcuts.append(shortcut)
ShortcutsService._save_config(config)
return shortcut
@staticmethod
def update_shortcut(shortcut_id: str, shortcut: ServiceShortcut) -> ServiceShortcut:
"""Met à jour un raccourci existant"""
config = ShortcutsService._load_config()
for i, s in enumerate(config.shortcuts):
if s.id == shortcut_id:
shortcut.id = shortcut_id # Garder l'ID
config.shortcuts[i] = shortcut
ShortcutsService._save_config(config)
return shortcut
raise Exception(f"Raccourci avec l'ID '{shortcut_id}' non trouvé")
@staticmethod
def delete_shortcut(shortcut_id: str) -> dict:
"""Supprime un raccourci"""
config = ShortcutsService._load_config()
initial_count = len(config.shortcuts)
config.shortcuts = [s for s in config.shortcuts if s.id != shortcut_id]
if len(config.shortcuts) == initial_count:
raise Exception(f"Raccourci avec l'ID '{shortcut_id}' non trouvé")
# Réorganiser les ordres
for i, s in enumerate(config.shortcuts):
s.order = i
ShortcutsService._save_config(config)
return {"message": "Raccourci supprimé", "id": shortcut_id}
@staticmethod
def reorder_shortcuts(shortcut_ids: List[str]) -> List[ServiceShortcut]:
"""Réordonne les raccourcis"""
config = ShortcutsService._load_config()
# Créer un dict pour accès rapide
shortcuts_dict = {s.id: s for s in config.shortcuts}
# Réorganiser selon l'ordre donné
reordered = []
for i, shortcut_id in enumerate(shortcut_ids):
if shortcut_id in shortcuts_dict:
s = shortcuts_dict[shortcut_id]
s.order = i
reordered.append(s)
config.shortcuts = reordered
ShortcutsService._save_config(config)
return reordered
@staticmethod
def export_shortcuts() -> dict:
"""Exporte les raccourcis en JSON"""
config = ShortcutsService._load_config()
return config.model_dump()
@staticmethod
def import_shortcuts(shortcuts_data: List[dict]) -> List[ServiceShortcut]:
"""Importe des raccourcis depuis JSON"""
config = ShortcutsConfig()
for i, data in enumerate(shortcuts_data):
try:
shortcut = ServiceShortcut(**data)
shortcut.order = i
config.shortcuts.append(shortcut)
except Exception as e:
print(f"Erreur lors de l'import du raccourci {i}: {e}")
ShortcutsService._save_config(config)
return config.shortcuts

View File

@@ -0,0 +1,310 @@
import psutil
import json
import subprocess
from typing import List, Optional, Dict, Any
from pydantic import BaseModel
class CPUUsage(BaseModel):
percent: float
average: float
cores: int
per_cpu: List[float]
freq: float # GHz
class MemoryUsage(BaseModel):
percent: float
used: int
total: int
available: int
cached: int
class DiskUsage(BaseModel):
device: str
total: int
used: int
free: int
percent: float
class NetworkUsage(BaseModel):
bytes_sent: int
bytes_recv: int
packets_sent: int
packets_recv: int
class ProcessInfo(BaseModel):
pid: int
name: str
status: str
cpu_percent: float
memory_percent: float
memory_mb: float
username: str
class BlockDevicePartition(BaseModel):
"""Représente une partition d'un disque"""
name: str
type: str
size: str
used: str
available: str
percent_used: float
mountpoint: Optional[str] = None
class BlockDevice(BaseModel):
"""Représente un disque ou un périphérique bloc"""
name: str
type: str
size: str
used: str
available: str
percent_used: float
mountpoint: Optional[str] = None
partitions: List[BlockDevicePartition] = []
class BlockDevicesInfo(BaseModel):
"""Informations sur tous les disques et partitions"""
devices: List[BlockDevice]
total_size: str
total_used: str
total_available: str
class SystemStats(BaseModel):
cpu: CPUUsage
memory: MemoryUsage
disk: List[DiskUsage]
network: NetworkUsage
processes: List[ProcessInfo]
class SystemService:
"""Service pour récupérer les informations système"""
@staticmethod
def get_cpu_usage() -> CPUUsage:
"""Récupère l'utilisation CPU"""
per_cpu = psutil.cpu_percent(interval=0, percpu=True)
avg_cpu = sum(per_cpu) / len(per_cpu) if per_cpu else 0
try:
freq = psutil.cpu_freq().current / 1000 # GHz
except:
freq = 0
return CPUUsage(
percent=avg_cpu,
average=avg_cpu,
cores=psutil.cpu_count(),
per_cpu=per_cpu,
freq=freq
)
@staticmethod
def get_memory_usage() -> MemoryUsage:
"""Récupère l'utilisation mémoire"""
mem = psutil.virtual_memory()
return MemoryUsage(
percent=mem.percent,
used=mem.used,
total=mem.total,
available=mem.available,
cached=mem.cached or 0
)
@staticmethod
def get_disk_usage() -> List[DiskUsage]:
"""Récupère l'utilisation disque"""
disks = []
try:
for partition in psutil.disk_partitions(all=False):
try:
usage = psutil.disk_usage(partition.mountpoint)
disks.append(DiskUsage(
device=partition.device,
total=usage.total,
used=usage.used,
free=usage.free,
percent=usage.percent
))
except (PermissionError, OSError):
continue
except:
pass
return disks
@staticmethod
def get_network_usage() -> NetworkUsage:
"""Récupère les stats réseau"""
net = psutil.net_io_counters()
return NetworkUsage(
bytes_sent=net.bytes_sent,
bytes_recv=net.bytes_recv,
packets_sent=net.packets_sent,
packets_recv=net.packets_recv
)
@staticmethod
def get_top_processes(limit: int = 10) -> List[ProcessInfo]:
"""Récupère les processus les plus actifs"""
processes = []
try:
for proc in psutil.process_iter(['pid', 'name', 'status', 'username']):
try:
info = proc.info
cpu_percent = proc.cpu_percent(interval=0)
mem_percent = proc.memory_percent()
mem_mb = proc.memory_info().rss / 1024 / 1024
# Ne garder que les processus avec activité
if cpu_percent > 0.5 or mem_percent > 0.5:
processes.append(ProcessInfo(
pid=info['pid'],
name=info['name'],
status=info['status'],
cpu_percent=cpu_percent,
memory_percent=mem_percent,
memory_mb=mem_mb,
username=info['username'] or 'N/A'
))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
# Trier par CPU + mémoire
processes.sort(key=lambda x: (x.cpu_percent + x.memory_percent), reverse=True)
return processes[:limit]
except Exception:
return []
@staticmethod
def get_system_stats() -> SystemStats:
"""Récupère toutes les stats système"""
return SystemStats(
cpu=SystemService.get_cpu_usage(),
memory=SystemService.get_memory_usage(),
disk=SystemService.get_disk_usage(),
network=SystemService.get_network_usage(),
processes=SystemService.get_top_processes()
)
@staticmethod
def format_bytes(bytes_val: int) -> str:
"""Convertit les bytes en format lisible"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_val < 1024:
return f"{bytes_val:.2f} {unit}"
bytes_val /= 1024
return f"{bytes_val:.2f} PB"
@staticmethod
def get_block_devices() -> BlockDevicesInfo:
"""Récupère les disques et partitions avec lsblk"""
try:
# Exécuter lsblk avec sortie JSON
result = subprocess.run(
['lsblk', '--json', '--bytes', '--output', 'NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
raise Exception(f"lsblk failed: {result.stderr}")
lsblk_data = json.loads(result.stdout)
devices = []
total_size = 0
total_used = 0
total_available = 0
for block_device in lsblk_data.get('blockdevices', []):
# Obtenir les informations d'utilisation pour ce disque
device_name = f"/dev/{block_device['name']}"
size = block_device.get('size', 0)
used = 0
available = 0
percent_used = 0
mountpoint = None
partitions = []
# Essayer d'obtenir les stats d'utilisation
try:
# Chercher le premier mountpoint
if block_device.get('mountpoints'):
mountpoint = block_device['mountpoints'][0]
if mountpoint:
disk_usage = psutil.disk_usage(mountpoint)
used = disk_usage.used
available = disk_usage.free
percent_used = disk_usage.percent
total_used += used
total_available += available
except:
pass
# Traiter les partitions
if 'children' in block_device:
for child in block_device['children']:
child_device = f"/dev/{child['name']}"
child_size = child.get('size', 0)
child_used = 0
child_available = 0
child_percent = 0
child_mountpoint = None
try:
if child.get('mountpoints'):
child_mountpoint = child['mountpoints'][0]
if child_mountpoint:
disk_usage = psutil.disk_usage(child_mountpoint)
child_used = disk_usage.used
child_available = disk_usage.free
child_percent = disk_usage.percent
except:
pass
partitions.append(BlockDevicePartition(
name=child['name'],
type=child.get('type', 'unknown'),
size=SystemService.format_bytes(child_size),
used=SystemService.format_bytes(child_used),
available=SystemService.format_bytes(child_available),
percent_used=child_percent,
mountpoint=child_mountpoint
))
total_size += size
devices.append(BlockDevice(
name=block_device['name'],
type=block_device.get('type', 'unknown'),
size=SystemService.format_bytes(size),
used=SystemService.format_bytes(used),
available=SystemService.format_bytes(available),
percent_used=percent_used,
mountpoint=mountpoint,
partitions=partitions
))
return BlockDevicesInfo(
devices=devices,
total_size=SystemService.format_bytes(total_size),
total_used=SystemService.format_bytes(total_used),
total_available=SystemService.format_bytes(total_available)
)
except Exception as e:
# Si lsblk échoue, retourner une liste vide
return BlockDevicesInfo(
devices=[],
total_size="0 B",
total_used="0 B",
total_available="0 B"
)

View File

@@ -0,0 +1,101 @@
import psutil
from typing import List, Dict
from pydantic import BaseModel
class CPUUsage(BaseModel):
percent: float
average: float
cores: int
per_cpu: List[float]
class MemoryUsage(BaseModel):
percent: float
used: int # en bytes
total: int # en bytes
available: int # en bytes
class ProcessInfo(BaseModel):
pid: int
name: str
status: str
cpu_percent: float
memory_percent: float
username: str
class SystemStats(BaseModel):
cpu: CPUUsage
memory: MemoryUsage
processes: List[ProcessInfo]
class SystemService:
"""Service pour récupérer les informations système"""
@staticmethod
def get_cpu_usage() -> CPUUsage:
"""Récupère l'utilisation CPU (non-bloquant, utilise le cache)"""
per_cpu = psutil.cpu_percent(interval=0, percpu=True)
avg_cpu = sum(per_cpu) / len(per_cpu) if per_cpu else 0
return CPUUsage(
percent=avg_cpu,
average=avg_cpu,
cores=psutil.cpu_count(),
per_cpu=per_cpu
)
@staticmethod
def get_memory_usage() -> MemoryUsage:
"""Récupère l'utilisation mémoire"""
mem = psutil.virtual_memory()
return MemoryUsage(
percent=mem.percent,
used=mem.used,
total=mem.total,
available=mem.available
)
@staticmethod
def get_top_processes(limit: int = 5) -> List[ProcessInfo]:
"""Récupère les processus les plus actifs (optimisé)"""
processes = []
try:
for proc in psutil.process_iter(['pid', 'name', 'status', 'username']):
try:
info = proc.info
cpu_percent = proc.cpu_percent(interval=0)
# Ne garder que les processus avec une activité CPU > 0
if cpu_percent > 0.1:
mem_percent = proc.memory_percent()
processes.append(ProcessInfo(
pid=info['pid'],
name=info['name'],
status=info['status'],
cpu_percent=cpu_percent,
memory_percent=mem_percent,
username=info['username'] or 'N/A'
))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
# Trier par utilisation CPU
processes.sort(key=lambda x: x.cpu_percent, reverse=True)
return processes[:limit]
except Exception as e:
return []
return []
print(f"Erreur lors de la récupération des processus: {e}")
return []
@staticmethod
def get_system_stats() -> SystemStats:
"""Récupère les statistiques système complètes"""
return SystemStats(
cpu=SystemService.get_cpu_usage(),
memory=SystemService.get_memory_usage(),
processes=SystemService.get_top_processes(15)
)

69
backend/main.py Normal file
View File

@@ -0,0 +1,69 @@
"""
InnotexBoard - Interface d'administration Debian
Backend FastAPI
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
import uvicorn
from app.core.config import settings
from app.api.routes import api_router
from app.api.websocket import router as ws_router
# Initialiser l'application FastAPI
app = FastAPI(
title=settings.API_TITLE,
description=settings.API_DESCRIPTION,
version=settings.API_VERSION,
)
# Middleware de sécurité CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Middleware pour les hôtes de confiance
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["localhost", "127.0.0.1"],
)
# Inclure les routes API
app.include_router(api_router, prefix="/api/v1")
app.include_router(ws_router, prefix="/api/v1")
@app.get("/")
async def root():
"""Endpoint racine"""
return {
"message": "Bienvenue sur InnotexBoard",
"version": settings.API_VERSION,
"docs": "/docs",
"openapi": "/openapi.json"
}
@app.get("/health")
async def health_check():
"""Vérification de la santé de l'application"""
return {
"status": "healthy",
"service": "InnotexBoard API"
}
if __name__ == "__main__":
# Configuration pour le développement
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
reload=True,
log_level="info",
)

13
backend/requirements.txt Normal file
View File

@@ -0,0 +1,13 @@
fastapi>=0.100.0
uvicorn>=0.24.0
python-jose>=3.3.0
python-multipart>=0.0.6
pydantic>=2.5.0
pydantic-settings>=2.1.0
python-pam>=2.0.2
psutil>=5.9.0
docker>=7.0.0
PyJWT>=2.8.0
passlib>=1.7.4
cryptography>=40.0.0
python-dotenv>=1.0.0