Initial commit
This commit is contained in:
4
backend/.env.example
Normal file
4
backend/.env.example
Normal 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
15
backend/.gitignore
vendored
Normal 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
21
backend/Dockerfile
Normal 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
38
backend/README.md
Normal 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
0
backend/app/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/endpoints/__init__.py
Normal file
0
backend/app/api/endpoints/__init__.py
Normal file
51
backend/app/api/endpoints/auth.py
Normal file
51
backend/app/api/endpoints/auth.py
Normal 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"}
|
||||
119
backend/app/api/endpoints/docker.py
Normal file
119
backend/app/api/endpoints/docker.py
Normal 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é"}
|
||||
68
backend/app/api/endpoints/packages.py
Normal file
68
backend/app/api/endpoints/packages.py
Normal 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)
|
||||
94
backend/app/api/endpoints/shortcuts.py
Normal file
94
backend/app/api/endpoints/shortcuts.py
Normal 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)}
|
||||
35
backend/app/api/endpoints/system.py
Normal file
35
backend/app/api/endpoints/system.py
Normal 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
10
backend/app/api/routes.py
Normal 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"])
|
||||
59
backend/app/api/websocket.py
Normal file
59
backend/app/api/websocket.py
Normal 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)
|
||||
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
37
backend/app/core/config.py
Normal file
37
backend/app/core/config.py
Normal 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()
|
||||
83
backend/app/core/security.py
Normal file
83
backend/app/core/security.py
Normal 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)
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
153
backend/app/services/docker_service.py
Normal file
153
backend/app/services/docker_service.py
Normal 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
|
||||
262
backend/app/services/packages.py
Normal file
262
backend/app/services/packages.py
Normal 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)}")
|
||||
171
backend/app/services/shortcuts.py
Normal file
171
backend/app/services/shortcuts.py
Normal 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
|
||||
310
backend/app/services/system.py
Normal file
310
backend/app/services/system.py
Normal 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"
|
||||
)
|
||||
101
backend/app/services/system_old.py
Normal file
101
backend/app/services/system_old.py
Normal 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
69
backend/main.py
Normal 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
13
backend/requirements.txt
Normal 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
|
||||
Reference in New Issue
Block a user