Remise en place dans un dossier

This commit is contained in:
Guillaume-Sanchez
2026-06-29 21:29:37 +02:00
parent 2aad2e65cc
commit d764335741
9 changed files with 8 additions and 4 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

+259
View File
@@ -0,0 +1,259 @@
<!DOCTYPE html>
<html lang="fr" class="h-full bg-gray-50">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cyberusgate - Démonstration</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<!-- Utilisation de Tailwind CSS via CDN pour un prototypage rapide -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="h-full">
<div class="flex flex-col md:flex-row w-full h-full min-h-screen font-sans">
<!-- ======================================= -->
<!-- PANNEAU 1: CONSOLE D'ADMIN -->
<!-- ======================================= -->
<div class="w-full md:w-2/3 lg:w-3/5 bg-white p-6 md:p-8 border-r border-gray-200">
<header class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">Cyberusgate</h1>
<p class="text-gray-500">Console d'administration centralisée</p>
</header>
<!-- Section de gestion des utilisateurs -->
<div class="mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-700">Utilisateurs Actifs</h2>
<button onclick="createEphemeralPass()" class="px-4 py-2 bg-blue-600 text-white rounded-lg shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition">
Créer un pass éphémère
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full bg-white border border-gray-200 rounded-lg">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nom</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rôle</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody id="user-table-body" class="divide-y divide-gray-200">
<!-- Les lignes des utilisateurs seront injectées ici par JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Section du journal d'événements -->
<div>
<h2 class="text-xl font-semibold text-gray-700 mb-4">Journal d'Événements en Temps Réel</h2>
<div id="log-container" class="h-64 bg-gray-800 text-white font-mono text-sm p-4 rounded-lg overflow-y-auto flex flex-col-reverse">
<!-- Les logs seront injectés ici par JavaScript -->
</div>
</div>
</div>
<!-- ======================================= -->
<!-- PANNEAU 2: SIMULATEUR DE PORTE -->
<!-- ======================================= -->
<div class="w-full md:w-1/3 lg:w-2/5 bg-gray-100 p-6 md:p-8 flex flex-col justify-center items-center">
<div class="w-full max-w-sm text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Simulateur de Porte</h2>
<!-- Le "lecteur" visuel -->
<div id="access-reader" class="relative w-48 h-64 bg-gray-700 rounded-lg shadow-2xl mx-auto flex flex-col justify-center items-center p-4 transition-all duration-300">
<div id="access-light" class="w-12 h-12 rounded-full bg-gray-500 mb-4 transition-colors duration-300"></div>
<p class="text-white font-bold text-lg">CYBERUSGATE</p>
<div id="access-result" class="absolute bottom-4 inset-x-0 text-white text-lg font-bold opacity-0 transition-opacity duration-300">
<!-- Résultat: "AUTORISÉ" ou "REFUSÉ" -->
</div>
</div>
<div class="mt-8">
<label for="user-selector" class="block text-sm font-medium text-gray-700 mb-2">Qui tente d'accéder ?</label>
<select id="user-selector" class="w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-indigo-500 focus:border-indigo-500">
<!-- Les options seront injectées ici -->
</select>
</div>
<div class="mt-6">
<button onclick="simulateScan()" class="w-full px-6 py-4 bg-indigo-600 text-white font-bold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-opacity-75 transition transform hover:scale-105">
Simuler un scan de badge
</button>
</div>
</div>
</div>
</div>
<!-- ======================================= -->
<!-- LOGIQUE JAVASCRIPT -->
<!-- ======================================= -->
<script>
const API_BASE_URL = 'http://127.0.0.1:8000';
const WEBSOCKET_URL = 'ws://127.0.0.1:8000/ws/logs';
// --- Éléments du DOM ---
const userTableBody = document.getElementById('user-table-body');
const userSelector = document.getElementById('user-selector');
const logContainer = document.getElementById('log-container');
const accessReader = document.getElementById('access-reader');
const accessLight = document.getElementById('access-light');
const accessResult = document.getElementById('access-result');
/**
* Au chargement de la page, on initialise les données et la connexion WebSocket.
*/
document.addEventListener('DOMContentLoaded', () => {
fetchUsersAndPopulate();
connectWebSocket();
});
/**
* Récupère les utilisateurs depuis l'API et met à jour l'interface (tableau et sélecteur).
*/
async function fetchUsersAndPopulate() {
try {
const response = await fetch(`${API_BASE_URL}/users`);
const users = await response.json();
// Vide les listes actuelles
userTableBody.innerHTML = '';
userSelector.innerHTML = '';
// Remplit le tableau et le sélecteur
users.forEach(user => {
// Pour le tableau d'admin
const accessStatus = user.has_access
? '<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Actif</span>'
: '<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Révoqué</span>';
const revokeButton = user.has_access
? `<button onclick="revokeAccess('${user.id}')" class="text-red-600 hover:text-red-900 font-medium">Révoquer</button>`
: `<span class="text-gray-400">N/A</span>`;
const tableRow = `
<tr id="user-row-${user.id}">
<td class="px-6 py-4 whitespace-nowrap"><div class="text-sm font-medium text-gray-900">${user.name}</div></td>
<td class="px-6 py-4 whitespace-nowrap"><div class="text-sm text-gray-500">${user.role}</div></td>
<td class="px-6 py-4 whitespace-nowrap text-center">${accessStatus}</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm">${revokeButton}</td>
</tr>`;
userTableBody.innerHTML += tableRow;
// Pour le sélecteur du simulateur
const selectorOption = `<option value="${user.id}">${user.name}</option>`;
userSelector.innerHTML += selectorOption;
});
} catch (error) {
console.error("Erreur lors de la récupération des utilisateurs:", error);
logMessage("Erreur: Impossible de joindre le serveur backend.", 'error');
}
}
/**
* Établit la connexion WebSocket pour recevoir les logs en temps réel.
*/
function connectWebSocket() {
const socket = new WebSocket(WEBSOCKET_URL);
socket.onmessage = (event) => {
logMessage(event.data, 'info');
};
socket.onclose = () => {
console.log("WebSocket déconnecté. Tentative de reconnexion dans 5s...");
logMessage("Serveur de logs déconnecté. Reconnexion...", 'error');
setTimeout(connectWebSocket, 5000); // Tente de se reconnecter
};
socket.onerror = () => {
console.error("Erreur WebSocket.");
logMessage("Erreur de connexion au serveur de logs.", 'error');
};
}
/**
* Affiche un message dans la console de logs.
*/
function logMessage(message, type = 'info') {
const logEntry = document.createElement('p');
logEntry.textContent = message;
if (message.includes("REFUSÉ")) {
logEntry.className = 'text-red-400';
} else if (message.includes("AUTORISÉ")) {
logEntry.className = 'text-green-400';
} else if (message.includes("ACTION ADMIN")) {
logEntry.className = 'text-yellow-400';
} else if (type === 'error') {
logEntry.className = 'text-red-500 font-bold';
}
logContainer.prepend(logEntry); // prepend pour avoir le plus récent en haut
}
/**
* Appelle l'API pour révoquer l'accès d'un utilisateur.
*/
async function revokeAccess(userId) {
if (!confirm(`Êtes-vous sûr de vouloir révoquer l'accès pour cet utilisateur ?`)) return;
await fetch(`${API_BASE_URL}/users/revoke/${userId}`, { method: 'POST' });
// On rafraîchit la liste pour mettre à jour le statut visuel partout
fetchUsersAndPopulate();
}
/**
* Appelle l'API pour créer un nouvel utilisateur éphémère.
*/
async function createEphemeralPass() {
await fetch(`${API_BASE_URL}/users/create_ephemeral`, { method: 'POST' });
fetchUsersAndPopulate();
}
/**
* Simule le scan de badge en appelant l'API.
*/
async function simulateScan() {
const selectedUserId = userSelector.value;
const response = await fetch(`${API_BASE_URL}/access/simulate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: selectedUserId })
});
const result = await response.json();
// Met à jour l'interface du lecteur
updateReaderUI(result.status);
}
/**
* Met à jour l'UI du lecteur (lumière et texte) après une tentative d'accès.
*/
function updateReaderUI(status) {
// Réinitialise d'abord l'état
accessLight.classList.remove('bg-green-500', 'bg-red-500');
accessResult.classList.remove('opacity-100');
accessResult.textContent = '';
if (status === 'authorized') {
accessLight.classList.add('bg-green-500');
accessResult.textContent = 'AUTORISÉ';
accessResult.classList.add('opacity-100');
} else {
accessLight.classList.add('bg-red-500');
accessResult.textContent = 'REFUSÉ';
accessResult.classList.add('opacity-100');
}
// Revient à l'état initial après quelques secondes
setTimeout(() => {
accessLight.classList.remove('bg-green-500', 'bg-red-500');
accessResult.classList.remove('opacity-100');
}, 2500);
}
</script>
</body>
</html>
+131
View File
@@ -0,0 +1,131 @@
# main.py : Backend pour le POC Cyberusgate avec FastAPI
import asyncio
import datetime
import uuid
from typing import Dict, List
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
# --- Initialisation de l'application FastAPI ---
app = FastAPI(
title="Cyberusgate POC API",
description="API pour la démonstration de la solution de badge digital."
)
# --- Configuration CORS ---
# Permet au frontend (ouvert depuis un fichier local) de communiquer avec le backend.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Attention : En production, restreindre à l'URL du frontend.
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- "Base de données" en mémoire ---
# Utilisation d'un dictionnaire pour simuler une base de données d'utilisateurs.
# La clé est l'ID de l'utilisateur, la valeur contient ses informations.
users_db: Dict[str, Dict] = {
"user-001": {"id": "user-001", "name": "Alice Martin", "role": "Collaborateur", "has_access": True},
"user-002": {"id": "user-002", "name": "Bob Dubois", "role": "Prestataire", "has_access": True},
}
# --- Gestion des connexions WebSocket pour les logs en temps réel ---
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
""" Diffuse un message à tous les clients connectés. """
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
async def log_and_broadcast(message: str):
""" Formate un message de log avec un timestamp et le diffuse. """
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
log_message = f"[{timestamp}] {message}"
print(log_message) # Affiche aussi dans la console du serveur
await manager.broadcast(log_message)
# --- Endpoints de l'API ---
@app.get("/users", summary="Lister tous les utilisateurs")
async def get_users():
""" Récupère la liste de tous les utilisateurs enregistrés. """
return list(users_db.values())
@app.post("/access/simulate", summary="Simuler un scan de badge")
async def simulate_access(data: Dict):
"""
Vérifie si un utilisateur a l'accès autorisé et log le résultat.
"""
user_id = data.get("user_id")
if not user_id or user_id not in users_db:
await log_and_broadcast(f"ERREUR: Tentative d'accès avec un ID inconnu '{user_id}'.")
return {"status": "refused", "reason": "Utilisateur inconnu"}
user = users_db[user_id]
if user["has_access"]:
await log_and_broadcast(f"ACCÈS AUTORISÉ: {user['name']} ({user['role']}) à la porte principale.")
return {"status": "authorized", "user_name": user['name']}
else:
await log_and_broadcast(f"ACCÈS REFUSÉ: {user['name']} ({user['role']}) - Droits révoqués.")
return {"status": "refused", "user_name": user['name'], "reason": "Accès révoqué"}
@app.post("/users/revoke/{user_id}", summary="Révoquer l'accès d'un utilisateur")
async def revoke_access(user_id: str):
""" Désactive les droits d'accès pour un utilisateur donné. """
if user_id in users_db:
users_db[user_id]["has_access"] = False
user_name = users_db[user_id]['name']
await log_and_broadcast(f"ACTION ADMIN: Accès révoqué pour {user_name}.")
return {"status": "success", "user_id": user_id, "has_access": False}
return {"status": "error", "message": "Utilisateur non trouvé"}
@app.post("/users/create_ephemeral", summary="Créer un pass éphémère")
async def create_ephemeral_pass():
""" Crée un nouvel utilisateur 'Prestataire' avec un accès temporaire. """
new_id = f"guest-{uuid.uuid4().hex[:4]}"
new_user = {
"id": new_id,
"name": f"Visiteur {new_id}",
"role": "Pass Éphémère",
"has_access": True,
}
users_db[new_id] = new_user
await log_and_broadcast(f"ACTION ADMIN: Pass éphémère créé pour {new_user['name']}.")
return new_user
# --- Endpoint WebSocket pour les logs ---
@app.websocket("/ws/logs")
async def websocket_endpoint(websocket: WebSocket):
"""
Point de connexion pour les clients qui veulent recevoir les logs en temps réel.
"""
await manager.connect(websocket)
await log_and_broadcast("INFO: Un client de la console d'admin s'est connecté.")
try:
while True:
# On maintient la connexion ouverte pour envoyer des messages
# Le serveur n'attend pas de message du client ici.
await asyncio.sleep(1)
except WebSocketDisconnect:
manager.disconnect(websocket)
await log_and_broadcast("INFO: Un client de la console d'admin s'est déconnecté.")
# --- Point d'entrée pour lancer le serveur (avec uvicorn) ---
if __name__ == "__main__":
import uvicorn
print("Lancement du serveur de l'API Cyberusgate sur http://127.0.0.1:8000")
uvicorn.run(app, host="127.0.0.1", port=8000)
+3
View File
@@ -0,0 +1,3 @@
fastapi==0.111.0
uvicorn[standard]==0.29.0
python-multipart==0.0.9
+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}