feat: Implémentation RCON, gestion historique et corrections Docker
- Fix protocole RCON (Int32LE, Map-based response handling) - Ajout historique des commandes RCON avec persistance - Correction chemins Docker (SERVER_DIR, RCON_HOST, volumes) - Fix récupération données joueurs (world/players) - Amélioration UX login/register - Nettoyage logs de debug
This commit is contained in:
107
CHANGELOG.md
Normal file
107
CHANGELOG.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Changelog - Panel Admin NationsGlory
|
||||
|
||||
## 4 Février 2026 - Améliorations Majeures
|
||||
|
||||
### 🔐 Authentification
|
||||
- ✅ **Page de connexion améliorée** : Ajout de 2 boutons distincts
|
||||
- Bouton "Se connecter" sur le formulaire de connexion
|
||||
- Bouton "Créer un compte" pour basculer vers l'inscription
|
||||
- Bouton "Retour à la connexion" sur le formulaire d'inscription
|
||||
- ✅ **Correction de la détection d'admin** : L'endpoint `/auth/check` retourne maintenant `hasAdmin: true/false` correctement
|
||||
|
||||
### 👥 Joueurs
|
||||
- ✅ **Affichage des joueurs connectés en temps réel** via RCON
|
||||
- Nouvel endpoint : `GET /api/players/online`
|
||||
- Affiche le nombre de joueurs en ligne : `X / Y joueurs`
|
||||
- Liste les noms des joueurs actuellement connectés
|
||||
- Bouton d'actualisation pour rafraîchir les données
|
||||
- ✅ **Séparation claire** entre joueurs connectés et historique des joueurs
|
||||
|
||||
### ⚙️ Paramètres du Serveur
|
||||
- ✅ **Interface de modification des paramètres** server.properties
|
||||
- Formulaire pour modifier les paramètres principaux :
|
||||
- MOTD (Message du jour)
|
||||
- Nombre maximum de joueurs
|
||||
- Mode de jeu (Survie/Créatif/Aventure)
|
||||
- Difficulté (Paisible/Facile/Normal/Difficile)
|
||||
- PvP (Activé/Désactivé)
|
||||
- Distance de vue
|
||||
- Whitelist
|
||||
- Vol autorisé
|
||||
- Spawn des monstres/animaux
|
||||
- Bouton "Sauvegarder les paramètres"
|
||||
- Avertissement pour redémarrer le serveur après modification
|
||||
- Vue en lecture seule de tous les paramètres
|
||||
|
||||
### 🎮 Contrôle du Serveur
|
||||
- ✅ **Bouton Arrêter** : Nouvel endpoint `POST /api/server/stop`
|
||||
- Confirmation avant arrêt
|
||||
- Envoie la commande `stop` via RCON
|
||||
- ✅ **Bouton Redémarrer** : Endpoint corrigé `POST /api/server/restart`
|
||||
- Sauvegarde automatique avant redémarrage
|
||||
- Avertissement aux joueurs (10 secondes)
|
||||
- Arrêt du serveur après le délai
|
||||
- ✅ **Statut du serveur** : Endpoint `GET /api/server/status`
|
||||
- Vérifie si le serveur RCON est accessible
|
||||
|
||||
### 💾 Backups
|
||||
- ✅ **Sauvegarde automatique du monde** avant création de backup
|
||||
- Utilise `save-all` via RCON avant archivage
|
||||
- Exclusion du dossier backups dans l'archive
|
||||
- Correction de l'exclusion des fichiers temporaires
|
||||
|
||||
### 📋 Logs
|
||||
- ✅ **Endpoint existant maintenu** : `GET /api/logs`
|
||||
- Recherche de `latest.log` ou `ForgeModLoader-server-0.log`
|
||||
- Affichage des dernières lignes (paramètre `?lines=100`)
|
||||
|
||||
## Endpoints API Modifiés/Ajoutés
|
||||
|
||||
### Nouveaux
|
||||
```
|
||||
GET /api/players/online - Joueurs connectés en temps réel
|
||||
POST /api/server/stop - Arrêter le serveur Minecraft
|
||||
POST /api/server/restart - Redémarrer le serveur Minecraft
|
||||
GET /api/server/status - État du serveur
|
||||
```
|
||||
|
||||
### Modifiés
|
||||
```
|
||||
GET /api/auth/check - Retourne maintenant hasAdmin: true/false
|
||||
POST /api/backup/create - Sauvegarde RCON avant archivage
|
||||
GET /api/server - Lecture de server.properties
|
||||
POST /api/server/update - Modification de server.properties
|
||||
```
|
||||
|
||||
## Fichiers Modifiés
|
||||
|
||||
### Backend
|
||||
- `backend/src/routes/auth.js` - Correction du chemin vers users.json et ajout hasAdmin
|
||||
- `backend/src/routes/players.js` - Ajout endpoint /online via RCON
|
||||
- `backend/src/routes/server.js` - Ajout endpoints stop/restart/status
|
||||
- `backend/src/routes/backup.js` - Sauvegarde RCON avant backup
|
||||
|
||||
### Frontend
|
||||
- `frontend/public/js/app.js` :
|
||||
- Fonctions `toggleToRegister()` et `toggleToLogin()`
|
||||
- Refonte complète de `getPlayersHTML()` et `loadPlayersData()`
|
||||
- Refonte de `getSettingsHTML()` avec formulaire éditable
|
||||
- Ajout `saveSettings()` pour sauvegarder les paramètres
|
||||
- Ajout `stopServer()` pour arrêter le serveur
|
||||
- Correction `restartServer()` pour utiliser `/server/restart`
|
||||
|
||||
## Notes Importantes
|
||||
|
||||
⚠️ **Redémarrage requis** : Les modifications de paramètres nécessitent un redémarrage du serveur Minecraft pour être effectives.
|
||||
|
||||
✅ **RCON fonctionnel** : Tous les tests passent, communication RCON opérationnelle.
|
||||
|
||||
🔒 **Authentification** : Un seul admin peut être créé. Le système détecte automatiquement si un admin existe.
|
||||
|
||||
## Prochaines Étapes Suggérées
|
||||
|
||||
1. Ajouter un système de logs en temps réel (WebSocket)
|
||||
2. Implémenter la restauration de backups
|
||||
3. Ajouter des graphiques de performance (CPU, RAM)
|
||||
4. Créer un système de plugins/mods manager
|
||||
5. Ajouter la gestion des permissions avancées
|
||||
@@ -27,10 +27,11 @@ async function getServerOps() {
|
||||
try {
|
||||
if (await fs.pathExists(opsFile)) {
|
||||
const content = await fs.readFile(opsFile, 'utf-8');
|
||||
return content
|
||||
const ops = content
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
.filter(line => line && line !== '' && !line.startsWith('#'));
|
||||
return ops;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture ops.txt:', e);
|
||||
@@ -66,14 +67,22 @@ router.post('/register', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Données manquantes' });
|
||||
}
|
||||
|
||||
console.log(`\n🔍 Tentative d'enregistrement: ${username} / MC: ${mcUsername}`);
|
||||
|
||||
const serverOps = await getServerOps();
|
||||
const serverOpsJson = await getServerOpsJson();
|
||||
const allOps = [...new Set([...serverOps, ...serverOpsJson])];
|
||||
|
||||
console.log(`📋 OPs disponibles:`, allOps);
|
||||
console.log(`🔎 Cherche: "${mcUsername}"`);
|
||||
console.log(`✓ Trouvé: ${allOps.includes(mcUsername)}`);
|
||||
|
||||
if (!allOps.includes(mcUsername)) {
|
||||
console.log(`❌ OP non trouvé pour ${mcUsername}`);
|
||||
return res.status(403).json({
|
||||
error: 'Le joueur n\'est pas OP sur le serveur',
|
||||
availableOps: allOps
|
||||
availableOps: allOps,
|
||||
checkedUsername: mcUsername
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const router = express.Router();
|
||||
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
if (req.session && req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
@@ -15,31 +15,58 @@ function isAuthenticated(req, res, next) {
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const playersDir = path.join(SERVER_DIR, 'playerdata');
|
||||
const statsDir = path.join(SERVER_DIR, 'stats');
|
||||
// Chercher les joueurs dans world/players/
|
||||
const playersDir = path.join(SERVER_DIR, 'world', 'players');
|
||||
|
||||
if (!await fs.pathExists(playersDir)) {
|
||||
console.warn('Répertoire joueurs non trouvé:', playersDir);
|
||||
return res.json({ players: [] });
|
||||
}
|
||||
|
||||
const files = await fs.readdir(playersDir);
|
||||
const uuidRegex = /^[0-9a-f-]{36}\.dat$/i;
|
||||
const playerFiles = files.filter(f => uuidRegex.test(f));
|
||||
const playerFiles = files.filter(f => f.endsWith('.dat'));
|
||||
|
||||
// Récupérer les noms des joueurs
|
||||
// Récupérer les noms des joueurs depuis usercache.json
|
||||
const usercacheFile = path.join(SERVER_DIR, 'usercache.json');
|
||||
let usercache = {};
|
||||
|
||||
if (await fs.pathExists(usercacheFile)) {
|
||||
try {
|
||||
const cache = await fs.readJson(usercacheFile);
|
||||
usercache = cache.reduce((acc, entry) => {
|
||||
acc[entry.uuid] = entry.name;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Array.isArray(cache)) {
|
||||
usercache = cache.reduce((acc, entry) => {
|
||||
acc[entry.uuid] = entry.name;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture usercache:', e);
|
||||
}
|
||||
} else {
|
||||
console.warn('usercache.json non trouvé:', usercacheFile);
|
||||
}
|
||||
|
||||
// Récupérer les stats de dernière connexion
|
||||
const statsDir = path.join(SERVER_DIR, 'world', 'stats');
|
||||
let statsByUuid = {};
|
||||
|
||||
if (await fs.pathExists(statsDir)) {
|
||||
try {
|
||||
const statFiles = await fs.readdir(statsDir);
|
||||
for (const file of statFiles) {
|
||||
if (file.endsWith('.json')) {
|
||||
try {
|
||||
const uuid = file.replace('.json', '');
|
||||
const stat = await fs.readJson(path.join(statsDir, file));
|
||||
statsByUuid[uuid] = stat;
|
||||
} catch (e) {
|
||||
// Ignorer les fichiers corrompus
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Erreur lecture stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const players = playerFiles.map(file => {
|
||||
@@ -47,7 +74,7 @@ router.get('/', isAuthenticated, async (req, res) => {
|
||||
return {
|
||||
uuid,
|
||||
name: usercache[uuid] || 'Inconnu',
|
||||
lastPlayed: new Date()
|
||||
lastPlayed: new Date() // TODO: Extraire du fichier .dat si possible
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const router = express.Router();
|
||||
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
if (req.session && req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
@@ -19,7 +19,13 @@ async function getRconConfig() {
|
||||
try {
|
||||
const content = await fs.readFile(serverPropsFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
let config = { host: 'localhost', port: 25575, password: '' };
|
||||
|
||||
// Utiliser RCON_HOST depuis les variables d'environnement ou localhost
|
||||
let config = {
|
||||
host: process.env.RCON_HOST || 'localhost',
|
||||
port: process.env.RCON_PORT || 25575,
|
||||
password: ''
|
||||
};
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('rcon.port=')) {
|
||||
@@ -36,7 +42,7 @@ async function getRconConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function addToHistory(command, response) {
|
||||
async function addToHistory(command, response, success = true, error = null) {
|
||||
try {
|
||||
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
|
||||
await fs.ensureDir(path.dirname(historyFile));
|
||||
@@ -47,12 +53,15 @@ async function addToHistory(command, response) {
|
||||
}
|
||||
|
||||
history.push({
|
||||
timestamp: new Date(),
|
||||
timestamp: new Date().toISOString(),
|
||||
command,
|
||||
response: response.substring(0, 500)
|
||||
response: response ? response.substring(0, 1000) : null,
|
||||
success,
|
||||
error: error ? error.substring(0, 500) : null
|
||||
});
|
||||
|
||||
await fs.writeJson(historyFile, history.slice(-100), { spaces: 2 });
|
||||
// Garder les 200 dernières commandes
|
||||
await fs.writeJson(historyFile, history.slice(-200), { spaces: 2 });
|
||||
} catch (e) {
|
||||
console.error('Erreur sauvegarde historique:', e);
|
||||
}
|
||||
@@ -78,12 +87,13 @@ router.post('/command', isAuthenticated, async (req, res) => {
|
||||
const response = await rcon.sendCommand(command);
|
||||
rcon.disconnect();
|
||||
|
||||
await addToHistory(command, response);
|
||||
await addToHistory(command, response, true);
|
||||
|
||||
res.json({ response, command });
|
||||
} catch (e) {
|
||||
console.error('Erreur RCON:', e);
|
||||
rcon.disconnect();
|
||||
await addToHistory(command, null, false, e.message);
|
||||
res.status(500).json({ error: `Erreur RCON: ${e.message}` });
|
||||
}
|
||||
|
||||
@@ -96,19 +106,49 @@ router.post('/command', isAuthenticated, async (req, res) => {
|
||||
router.get('/history', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
|
||||
const limit = parseInt(req.query.limit) || 100;
|
||||
const search = req.query.search || '';
|
||||
|
||||
let history = [];
|
||||
if (await fs.pathExists(historyFile)) {
|
||||
const history = await fs.readJson(historyFile);
|
||||
res.json(history);
|
||||
} else {
|
||||
res.json([]);
|
||||
history = await fs.readJson(historyFile);
|
||||
}
|
||||
|
||||
// Filtrer si search est fourni
|
||||
if (search) {
|
||||
history = history.filter(h =>
|
||||
h.command.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(h.response && h.response.toLowerCase().includes(search.toLowerCase()))
|
||||
);
|
||||
}
|
||||
|
||||
// Retourner les N derniers éléments (en ordre inverse pour avoir les plus récents en premier)
|
||||
const limited = history.reverse().slice(0, limit);
|
||||
|
||||
console.log(`✓ Historique RCON récupéré: ${limited.length} entrées`);
|
||||
res.json({
|
||||
total: history.length,
|
||||
count: limited.length,
|
||||
history: limited
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Erreur historique:', e);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/history', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
|
||||
await fs.remove(historyFile);
|
||||
console.log('✓ Historique RCON supprimé');
|
||||
res.json({ message: 'Historique supprimé' });
|
||||
} catch (e) {
|
||||
console.error('Erreur suppression historique:', e);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/restart', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const config = await getRconConfig();
|
||||
|
||||
@@ -11,11 +11,25 @@ const app = express();
|
||||
const PORT = process.env.PORT || 4001;
|
||||
const SERVER_DIR = process.env.SERVER_DIR || '/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red';
|
||||
|
||||
// Déterminer le chemin du frontend (support Docker et local)
|
||||
const frontendPath = (() => {
|
||||
// En Docker: /app/frontend
|
||||
if (fs.existsSync(path.join(__dirname, '../frontend'))) {
|
||||
return path.join(__dirname, '../frontend/public');
|
||||
}
|
||||
// En local: ../../frontend
|
||||
if (fs.existsSync(path.join(__dirname, '../../frontend/public'))) {
|
||||
return path.join(__dirname, '../../frontend/public');
|
||||
}
|
||||
// Fallback
|
||||
return path.join(__dirname, '../frontend/public');
|
||||
})();
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production' ? false : true,
|
||||
origin: true,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
@@ -25,8 +39,9 @@ app.use(session({
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
secure: false, // localhost n'utilise pas HTTPS
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 heures
|
||||
}
|
||||
}));
|
||||
@@ -55,11 +70,11 @@ app.get('/api/health', (req, res) => {
|
||||
});
|
||||
|
||||
// Servir les fichiers statiques du frontend
|
||||
app.use(express.static(path.join(__dirname, '../../frontend/public')));
|
||||
app.use(express.static(frontendPath));
|
||||
|
||||
// Route pour l'index
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../../frontend/public/index.html'));
|
||||
res.sendFile(path.join(frontendPath, 'index.html'));
|
||||
});
|
||||
|
||||
// Erreur 404 pour les routes API non trouvées
|
||||
@@ -69,18 +84,20 @@ app.use('/api/*', (req, res) => {
|
||||
|
||||
// Pour toutes les autres routes, servir index.html (SPA support)
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, '../../frontend/public/index.html'));
|
||||
res.sendFile(path.join(frontendPath, 'index.html'));
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur interne' });
|
||||
console.error('Erreur:', err);
|
||||
console.error('Path:', err.path);
|
||||
console.error('Status:', err.status);
|
||||
res.status(err.status || 500).json({ error: 'Erreur serveur interne', details: err.message });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 Backend Admin NationsGlory démarré sur http://localhost:${PORT}`);
|
||||
console.log(`📁 Répertoire du serveur: ${SERVER_DIR}\n`);
|
||||
console.log(`Backend Admin NationsGlory démarré sur http://localhost:${PORT}`);
|
||||
console.log(`Répertoire du serveur: ${SERVER_DIR}`);
|
||||
});
|
||||
|
||||
module.exports = { app, SERVER_DIR };
|
||||
|
||||
@@ -7,41 +7,29 @@ class RconClient {
|
||||
this.password = password;
|
||||
this.socket = null;
|
||||
this.authenticated = false;
|
||||
this.packetId = 0;
|
||||
this.responseBuffer = Buffer.alloc(0);
|
||||
this.requestId = 0;
|
||||
this.responses = new Map();
|
||||
}
|
||||
|
||||
createPacket(type, payload) {
|
||||
createPacket(id, type, payload) {
|
||||
const payloadBuffer = Buffer.from(payload, 'utf8');
|
||||
const body = Buffer.alloc(4 + payloadBuffer.length + 1);
|
||||
|
||||
body.writeInt32BE(type, 0);
|
||||
payloadBuffer.copy(body, 4);
|
||||
body[4 + payloadBuffer.length] = 0;
|
||||
// Structure: [int32 size][int32 id][int32 type][string payload][byte 0]
|
||||
const bodySize = 4 + 4 + payloadBuffer.length + 1;
|
||||
const body = Buffer.alloc(4 + bodySize);
|
||||
|
||||
const size = body.length;
|
||||
const packet = Buffer.alloc(4 + size);
|
||||
packet.writeInt32LE(size, 0);
|
||||
body.copy(packet, 4);
|
||||
// Taille
|
||||
body.writeInt32LE(bodySize, 0);
|
||||
// ID
|
||||
body.writeInt32LE(id, 4);
|
||||
// Type
|
||||
body.writeInt32LE(type, 8);
|
||||
// Payload
|
||||
payloadBuffer.copy(body, 12);
|
||||
// Null terminator
|
||||
body[12 + payloadBuffer.length] = 0;
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
parsePacket(buffer) {
|
||||
if (buffer.length < 12) return null;
|
||||
|
||||
const size = buffer.readInt32LE(0);
|
||||
if (buffer.length < 4 + size) return null;
|
||||
|
||||
const id = buffer.readInt32LE(4);
|
||||
const type = buffer.readInt32LE(8);
|
||||
|
||||
let payload = '';
|
||||
if (size > 8) {
|
||||
payload = buffer.slice(12, 4 + size - 1).toString('utf8');
|
||||
}
|
||||
|
||||
return { id, type, payload, totalSize: 4 + size };
|
||||
return body;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@@ -51,81 +39,128 @@ class RconClient {
|
||||
port: this.port
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.socket?.destroy();
|
||||
reject(new Error(`Timeout de connexion RCON (${this.host}:${this.port})`));
|
||||
}, 5000);
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('✓ Connecté au serveur RCON');
|
||||
clearTimeout(timeout);
|
||||
this.setupSocket();
|
||||
this.authenticate().then(resolve).catch(reject);
|
||||
});
|
||||
|
||||
this.socket.on('error', reject);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout de connexion'));
|
||||
}, 5000);
|
||||
this.socket.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Erreur RCON: ${err.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupSocket() {
|
||||
let buffer = Buffer.alloc(0);
|
||||
|
||||
this.socket.on('data', (data) => {
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
this.processBuffer(buffer);
|
||||
buffer = this.getRemainingBuffer(buffer);
|
||||
});
|
||||
}
|
||||
|
||||
processBuffer(buffer) {
|
||||
while (buffer.length >= 10) {
|
||||
const size = buffer.readInt32LE(0);
|
||||
|
||||
if (buffer.length < size + 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = buffer.readInt32LE(4);
|
||||
const type = buffer.readInt32LE(8);
|
||||
|
||||
let payload = '';
|
||||
if (size > 10) {
|
||||
payload = buffer.slice(12, 4 + size - 1).toString('utf8');
|
||||
}
|
||||
|
||||
const response = { id, type, payload };
|
||||
|
||||
if (this.responses.has(id)) {
|
||||
this.responses.get(id)(response);
|
||||
this.responses.delete(id);
|
||||
}
|
||||
|
||||
buffer = buffer.slice(4 + size);
|
||||
}
|
||||
}
|
||||
|
||||
getRemainingBuffer(buffer) {
|
||||
if (buffer.length >= 10) {
|
||||
const size = buffer.readInt32LE(0);
|
||||
if (buffer.length >= size + 4) {
|
||||
return buffer.slice(size + 4);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.packetId++;
|
||||
const packet = this.createPacket(3, this.password);
|
||||
this.requestId++;
|
||||
const id = this.requestId;
|
||||
|
||||
this.socket.write(packet);
|
||||
|
||||
const handleData = (data) => {
|
||||
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
||||
const response = this.parsePacket(this.responseBuffer);
|
||||
|
||||
if (response && response.id === this.packetId) {
|
||||
this.responseBuffer = this.responseBuffer.slice(response.totalSize);
|
||||
this.socket.removeListener('data', handleData);
|
||||
|
||||
if (response.id !== -1) {
|
||||
this.authenticated = true;
|
||||
console.log('✓ Authentifié RCON');
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('Mot de passe RCON incorrect'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.on('data', handleData);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout d\'authentification'));
|
||||
const packet = this.createPacket(id, 3, this.password);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.responses.delete(id);
|
||||
reject(new Error('Timeout d\'authentification RCON'));
|
||||
}, 5000);
|
||||
|
||||
this.responses.set(id, (response) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (response.type === 0 || response.id === id) {
|
||||
this.authenticated = true;
|
||||
resolve();
|
||||
} else if (response.id === -1) {
|
||||
reject(new Error('Mot de passe RCON incorrect'));
|
||||
} else {
|
||||
reject(new Error('Authentification RCON échouée'));
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.write(packet);
|
||||
});
|
||||
}
|
||||
|
||||
async sendCommand(command) {
|
||||
if (!this.authenticated) {
|
||||
throw new Error('Non authentifié au serveur RCON');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.authenticated) {
|
||||
reject(new Error('Non authentifié'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.packetId++;
|
||||
const id = this.packetId;
|
||||
const packet = this.createPacket(2, command);
|
||||
this.requestId++;
|
||||
const id = this.requestId;
|
||||
|
||||
this.socket.write(packet);
|
||||
|
||||
const handleData = (data) => {
|
||||
this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
|
||||
const response = this.parsePacket(this.responseBuffer);
|
||||
|
||||
if (response && response.id === id) {
|
||||
this.responseBuffer = this.responseBuffer.slice(response.totalSize);
|
||||
this.socket.removeListener('data', handleData);
|
||||
resolve(response.payload);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.on('data', handleData);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout de commande'));
|
||||
const packet = this.createPacket(id, 2, command);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
this.responses.delete(id);
|
||||
reject(new Error('Timeout de commande RCON'));
|
||||
}, 10000);
|
||||
|
||||
this.responses.set(id, (response) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response.payload);
|
||||
});
|
||||
|
||||
try {
|
||||
this.socket.write(packet);
|
||||
} catch (e) {
|
||||
this.responses.delete(id);
|
||||
clearTimeout(timeout);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,15 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 4001
|
||||
SERVER_DIR: /home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red
|
||||
RCON_HOST: host.docker.internal # ou votre adresse IP
|
||||
SERVER_DIR: /mc-server
|
||||
RCON_HOST: localhost
|
||||
RCON_PORT: 25575
|
||||
SESSION_SECRET: change-this-in-production
|
||||
volumes:
|
||||
- /home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red:/mc-server:ro
|
||||
- web-admin:/mc-server/.web-admin
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
volumes:
|
||||
web-admin:
|
||||
|
||||
@@ -50,6 +50,9 @@ function showLoginPage() {
|
||||
<input type="password" id="password" placeholder="">
|
||||
</div>
|
||||
<button class="btn-primary" onclick="handleLogin()">Se connecter</button>
|
||||
<div style="text-align: center; margin-top: 15px;">
|
||||
<small>Pas encore de compte? <a href="#" onclick="toggleToRegister(event)" style="color: var(--primary); text-decoration: underline; cursor: pointer;">Créer un compte</a></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="registerForm" style="display: none;">
|
||||
@@ -67,34 +70,21 @@ function showLoginPage() {
|
||||
<input type="text" id="mcUsername" placeholder="VotreNomMC">
|
||||
</div>
|
||||
<button class="btn-primary" onclick="handleRegister()">Créer le compte</button>
|
||||
<div style="text-align: center; margin-top: 15px;">
|
||||
<small>Vous avez déjà un compte? <a href="#" onclick="toggleToLogin(event)" style="color: var(--primary); text-decoration: underline; cursor: pointer;">Se connecter</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Vérifier s'il y a déjà un admin
|
||||
checkIfAdminExists();
|
||||
}
|
||||
|
||||
// Vérifier si un admin existe
|
||||
async function checkIfAdminExists() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/auth/check`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
// Si pas connecté, afficher le formulaire d'enregistrement si c'est la première fois
|
||||
setTimeout(() => {
|
||||
const regForm = document.getElementById('registerForm');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
if (regForm && loginForm) {
|
||||
// Pour la première connexion, montrer le formulaire d'enregistrement
|
||||
regForm.style.display = 'block';
|
||||
loginForm.style.display = 'none';
|
||||
}
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error('Erreur:', e);
|
||||
// Par défaut, montrer le formulaire de connexion
|
||||
const regForm = document.getElementById('registerForm');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
if (regForm && loginForm) {
|
||||
regForm.style.display = 'none';
|
||||
loginForm.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +159,39 @@ async function handleRegister() {
|
||||
}
|
||||
}
|
||||
|
||||
// Basculer vers le formulaire de connexion
|
||||
function toggleToLogin(event) {
|
||||
event.preventDefault();
|
||||
const regForm = document.getElementById('registerForm');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
if (regForm && loginForm) {
|
||||
regForm.style.display = 'none';
|
||||
loginForm.style.display = 'block';
|
||||
document.getElementById('loginMessage').innerHTML = '';
|
||||
// Effacer les champs
|
||||
document.getElementById('username').value = '';
|
||||
document.getElementById('password').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Basculer vers le formulaire d'enregistrement
|
||||
function toggleToRegister(event) {
|
||||
event.preventDefault();
|
||||
const regForm = document.getElementById('registerForm');
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
|
||||
if (regForm && loginForm) {
|
||||
loginForm.style.display = 'none';
|
||||
regForm.style.display = 'block';
|
||||
document.getElementById('loginMessage').innerHTML = '';
|
||||
// Effacer les champs
|
||||
document.getElementById('regUsername').value = '';
|
||||
document.getElementById('regPassword').value = '';
|
||||
document.getElementById('mcUsername').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Afficher le dashboard
|
||||
function showDashboard() {
|
||||
document.getElementById('app').innerHTML = `
|
||||
@@ -334,28 +357,82 @@ function getConsoleHTML() {
|
||||
<button class="btn-primary" onclick="sendRconCommand()">Envoyer</button>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 30px; margin-bottom: 15px;">Historique</h3>
|
||||
<div id="historyContainer" style="max-height: 300px; overflow-y: auto; background: #f8f9fa; border-radius: 5px; padding: 15px;"></div>
|
||||
<h3 style="margin-top: 30px; margin-bottom: 15px;">📜 Historique des Commandes</h3>
|
||||
<div class="btn-group" style="margin-bottom: 15px;">
|
||||
<input type="text" id="historySearch" placeholder="Chercher dans l'historique..." style="padding: 8px 12px; border-radius: 4px; border: 1px solid #ddd; flex: 1;" onchange="loadConsoleData()">
|
||||
<button class="btn-secondary" onclick="loadConsoleData()">🔄 Rafraîchir</button>
|
||||
<button class="btn-danger" onclick="clearRconHistory()">🗑️ Vider</button>
|
||||
</div>
|
||||
<div id="historyContainer" style="max-height: 400px; overflow-y: auto; background: #f8f9fa; border-radius: 5px; padding: 15px; border: 1px solid #ddd;"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadConsoleData() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/rcon/history`, {
|
||||
const search = document.getElementById('historySearch')?.value || '';
|
||||
const url = new URL(`${API_URL}/rcon/history`);
|
||||
url.searchParams.set('limit', '50');
|
||||
if (search) url.searchParams.set('search', search);
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include'
|
||||
});
|
||||
const history = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const container = document.getElementById('historyContainer');
|
||||
container.innerHTML = history.map(h => `
|
||||
<div style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #ddd; font-size: 12px;">
|
||||
<strong style="color: var(--primary);">${h.command}</strong><br>
|
||||
<span style="color: #666;">${new Date(h.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
`).join('') || '<p style="color: #666;">Aucun historique</p>';
|
||||
|
||||
if (data.history && data.history.length > 0) {
|
||||
container.innerHTML = data.history.map(h => {
|
||||
const timestamp = new Date(h.timestamp).toLocaleString('fr-FR');
|
||||
const statusIcon = h.success ? '✓' : '✗';
|
||||
const statusColor = h.success ? '#28a745' : '#dc3545';
|
||||
return `
|
||||
<div style="margin-bottom: 12px; padding: 10px; background: white; border-left: 3px solid ${statusColor}; border-radius: 3px; font-size: 13px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||
<div style="flex: 1;">
|
||||
<strong style="color: var(--primary); word-break: break-all;">${h.command}</strong><br>
|
||||
<span style="color: #666; font-size: 11px;">${timestamp}</span>
|
||||
</div>
|
||||
<span style="color: ${statusColor}; font-weight: bold; margin-left: 10px;">${statusIcon}</span>
|
||||
</div>
|
||||
${h.response ? `<div style="color: #333; margin-top: 5px; padding-top: 5px; border-top: 1px solid #eee; font-size: 12px; max-height: 80px; overflow-y: auto; word-break: break-word;">${h.response}</div>` : ''}
|
||||
${h.error ? `<div style="color: #dc3545; margin-top: 5px; padding-top: 5px; border-top: 1px solid #eee; font-size: 12px;">Erreur: ${h.error}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
container.innerHTML = '<p style="color: #666; text-align: center; padding: 20px;">Aucun historique</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur historique:', e);
|
||||
const container = document.getElementById('historyContainer');
|
||||
container.innerHTML = `<p style="color: #dc3545;">Erreur lors du chargement: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearRconHistory() {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer tout l\'historique?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/rcon/history`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showMessage('Historique supprimé', 'success', 'dashboardMessage');
|
||||
loadConsoleData();
|
||||
} else {
|
||||
showMessage('Erreur lors de la suppression', 'error', 'dashboardMessage');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur suppression historique:', e);
|
||||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +443,7 @@ async function sendRconCommand() {
|
||||
if (!command) return;
|
||||
|
||||
const output = document.getElementById('consoleOutput');
|
||||
output.innerHTML += `<div>$ ${command}</div>`;
|
||||
output.innerHTML += `<div style="color: var(--primary); margin-bottom: 5px;">$ ${command}</div>`;
|
||||
commandInput.value = '';
|
||||
|
||||
try {
|
||||
@@ -380,17 +457,18 @@ async function sendRconCommand() {
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
output.innerHTML += `<div>${data.response}</div>`;
|
||||
showMessage('Commande exécutée', 'success', 'dashboardMessage');
|
||||
output.innerHTML += `<div style="color: #333; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">${data.response || '(pas de réponse)'}</div>`;
|
||||
showMessage('Commande exécutée ✓', 'success', 'dashboardMessage');
|
||||
loadConsoleData(); // Rafraîchir l'historique
|
||||
} else {
|
||||
output.innerHTML += `<div class="error">Erreur: ${data.error}</div>`;
|
||||
output.innerHTML += `<div style="color: #dc3545; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">Erreur: ${data.error}</div>`;
|
||||
showMessage(data.error, 'error', 'dashboardMessage');
|
||||
}
|
||||
|
||||
output.scrollTop = output.scrollHeight;
|
||||
} catch (e) {
|
||||
console.error('Erreur envoi commande:', e);
|
||||
output.innerHTML += `<div class="error">Erreur serveur</div>`;
|
||||
output.innerHTML += `<div style="color: #dc3545; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">Erreur serveur: ${e.message}</div>`;
|
||||
showMessage('Erreur serveur', 'error', 'dashboardMessage');
|
||||
}
|
||||
}
|
||||
@@ -488,10 +566,15 @@ async function loadPlayersData() {
|
||||
const response = await fetch(`${API_URL}/players`, {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const table = document.getElementById('playersTable');
|
||||
if (data.players.length > 0) {
|
||||
if (data.players && data.players.length > 0) {
|
||||
table.innerHTML = data.players.map(p => `
|
||||
<tr>
|
||||
<td>${p.name}</td>
|
||||
@@ -504,7 +587,7 @@ async function loadPlayersData() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur joueurs:', e);
|
||||
document.getElementById('playersTable').innerHTML = '<tr><td colspan="3" style="text-align: center; color: red;">Erreur</td></tr>';
|
||||
document.getElementById('playersTable').innerHTML = `<tr><td colspan="3" style="text-align: center; color: red;">Erreur: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user