Refonte total
This commit is contained in:
+12
-7
@@ -1,7 +1,12 @@
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY app/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app/ .
|
||||
EXPOSE 8000
|
||||
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
# Dockerfile.nginx (Frontend)
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copie vos fichiers statiques (index.html, etc.)
|
||||
COPY ./app /usr/share/nginx/html
|
||||
|
||||
# Force les droits Linux corrects pour Nginx (évite le 403 Forbidden)
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||
chmod -R 755 /usr/share/nginx/html
|
||||
|
||||
# Nginx écoute par défaut sur le port 80 dans le conteneur
|
||||
EXPOSE 80
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 989 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
+820
-241
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "SafeAccess — Gestion de badges",
|
||||
"short_name": "SafeAccess",
|
||||
"description": "Gestion de badges d'accès NFC, RFID, QR Code et Bluetooth avec 2FA",
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0D2B5E",
|
||||
"theme_color": "#1565C0",
|
||||
"orientation": "portrait",
|
||||
"icons": [
|
||||
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||
],
|
||||
"categories": ["security", "utilities", "business"],
|
||||
"lang": "fr",
|
||||
"scope": "/"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const CACHE = 'safeaccess-v1';
|
||||
const ASSETS = ['/', '/index.html', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(r => r || fetch(e.request)).catch(() => caches.match('/index.html'))
|
||||
);
|
||||
});
|
||||
+1
-12
@@ -1,20 +1,9 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: cyberusgate_backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
networks:
|
||||
- cyberusgate-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.nginx
|
||||
dockerfile: Dockerfile
|
||||
container_name: cyberusgate_frontend
|
||||
ports:
|
||||
- "8888:80"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY app/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app/ .
|
||||
EXPOSE 8000
|
||||
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
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 |
@@ -0,0 +1,265 @@
|
||||
<!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>
|
||||
// Production URLs
|
||||
const API_BASE_URL = 'https://cyberusgate.api.guillaume-sanchez.fr';
|
||||
const WEBSOCKET_URL = 'ws://cyberusgate.api.guillaume-sanchez.fr/ws/logs';
|
||||
// Dev URLs
|
||||
// const API_BASE_URL = 'http://192.168.10.10:8000';
|
||||
// const WEBSOCKET_URL = 'ws://192.168.10.10:8000/ws/logs';
|
||||
|
||||
cyberusgate.api
|
||||
|
||||
// --- É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>
|
||||
Executable → Regular
Executable → Regular
@@ -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"}
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: cyberusgate_backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
networks:
|
||||
- cyberusgate-network
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.nginx
|
||||
container_name: cyberusgate_frontend
|
||||
ports:
|
||||
- "8888:80"
|
||||
networks:
|
||||
- cyberusgate-network
|
||||
|
||||
networks:
|
||||
cyberusgate-network:
|
||||
driver: bridge
|
||||
Reference in New Issue
Block a user