initial commit
This commit is contained in:
6
backend/.env.example
Normal file
6
backend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
SESSION_SECRET=your-very-secret-session-key-change-in-production
|
||||
SERVER_DIR=/home/innotex/Documents/Projet/Serveur NationsGlory/NationsGlory_ServeurBuild_Red
|
||||
RCON_HOST=localhost
|
||||
RCON_PORT=25575
|
||||
25
backend/package.json
Normal file
25
backend/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "nations-glory-admin-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API pour l'interface web de gestion du serveur Minecraft NationsGlory",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js"
|
||||
},
|
||||
"keywords": ["minecraft", "admin", "api"],
|
||||
"author": "Admin",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"fs-extra": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.20"
|
||||
}
|
||||
}
|
||||
200
backend/src/routes/auth.js
Normal file
200
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,200 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
const USERS_FILE = path.join(__dirname, '../../data/users.json');
|
||||
|
||||
async function initUsersFile() {
|
||||
await fs.ensureDir(path.dirname(USERS_FILE));
|
||||
if (!await fs.pathExists(USERS_FILE)) {
|
||||
await fs.writeJson(USERS_FILE, [], { spaces: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
async function getServerOps() {
|
||||
const opsFile = path.join(SERVER_DIR, 'ops.txt');
|
||||
try {
|
||||
if (await fs.pathExists(opsFile)) {
|
||||
const content = await fs.readFile(opsFile, 'utf-8');
|
||||
return content
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture ops.txt:', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function getServerOpsJson() {
|
||||
const opsFile = path.join(SERVER_DIR, 'ops.json');
|
||||
try {
|
||||
if (await fs.pathExists(opsFile)) {
|
||||
const ops = await fs.readJson(opsFile);
|
||||
return ops.map(op => typeof op === 'string' ? op : op.name).filter(Boolean);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture ops.json:', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
await initUsersFile();
|
||||
const users = await fs.readJson(USERS_FILE);
|
||||
|
||||
if (users.length > 0) {
|
||||
return res.status(403).json({ error: 'Enregistrement déjà effectué' });
|
||||
}
|
||||
|
||||
const { username, password, mcUsername } = req.body;
|
||||
|
||||
if (!username || !password || !mcUsername) {
|
||||
return res.status(400).json({ error: 'Données manquantes' });
|
||||
}
|
||||
|
||||
const serverOps = await getServerOps();
|
||||
const serverOpsJson = await getServerOpsJson();
|
||||
const allOps = [...new Set([...serverOps, ...serverOpsJson])];
|
||||
|
||||
if (!allOps.includes(mcUsername)) {
|
||||
return res.status(403).json({
|
||||
error: 'Le joueur n\'est pas OP sur le serveur',
|
||||
availableOps: allOps
|
||||
});
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
users.push({
|
||||
id: Date.now().toString(),
|
||||
username,
|
||||
mcUsername,
|
||||
password: hashedPassword,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
await fs.writeJson(USERS_FILE, users, { spaces: 2 });
|
||||
|
||||
res.json({
|
||||
message: 'Admin créé avec succès',
|
||||
user: { username, mcUsername }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur enregistrement:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
await initUsersFile();
|
||||
const users = await fs.readJson(USERS_FILE);
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: 'Identifiants manquants' });
|
||||
}
|
||||
|
||||
const user = users.find(u => u.username === username);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Identifiants incorrects' });
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return res.status(401).json({ error: 'Identifiants incorrects' });
|
||||
}
|
||||
|
||||
req.session.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
mcUsername: user.mcUsername
|
||||
};
|
||||
|
||||
res.json({
|
||||
message: 'Connecté avec succès',
|
||||
user: req.session.user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur connexion:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/check', (req, res) => {
|
||||
if (req.session.user) {
|
||||
res.json({ authenticated: true, user: req.session.user });
|
||||
} else {
|
||||
res.json({ authenticated: false });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Erreur déconnexion' });
|
||||
}
|
||||
res.json({ message: 'Déconnecté' });
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/change-rcon-password', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { oldPassword, newPassword } = req.body;
|
||||
const serverPropsFile = path.join(SERVER_DIR, 'server.properties');
|
||||
|
||||
if (!oldPassword || !newPassword) {
|
||||
return res.status(400).json({ error: 'Données manquantes' });
|
||||
}
|
||||
|
||||
let content = await fs.readFile(serverPropsFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
let currentPassword = null;
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('rcon.password=')) {
|
||||
currentPassword = line.split('=')[1].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPassword !== oldPassword) {
|
||||
return res.status(401).json({ error: 'Ancien mot de passe RCON incorrect' });
|
||||
}
|
||||
|
||||
content = lines.map(line => {
|
||||
if (line.startsWith('rcon.password=')) {
|
||||
return `rcon.password=${newPassword}`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
await fs.writeFile(serverPropsFile, content, 'utf-8');
|
||||
|
||||
res.json({ message: 'Mot de passe RCON changé (redémarrage du serveur requis)' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur changement RCON:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
99
backend/src/routes/backup.js
Normal file
99
backend/src/routes/backup.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const backupDir = path.join(SERVER_DIR, 'backups');
|
||||
|
||||
if (!await fs.pathExists(backupDir)) {
|
||||
return res.json({ backups: [] });
|
||||
}
|
||||
|
||||
const files = await fs.readdir(backupDir);
|
||||
const backups = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = path.join(backupDir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
return {
|
||||
name: file,
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2) + ' MB',
|
||||
created: stats.mtime,
|
||||
path: filePath
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json(backups.sort((a, b) => new Date(b.created) - new Date(a.created)));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur backups:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/create', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const backupDir = path.join(SERVER_DIR, 'backups');
|
||||
await fs.ensureDir(backupDir);
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupName = `backup-${timestamp}.tar.gz`;
|
||||
const backupPath = path.join(backupDir, backupName);
|
||||
|
||||
res.json({ message: 'Backup en cours...', backupName });
|
||||
|
||||
// Exécuter la création de backup en arrière-plan
|
||||
exec(
|
||||
`cd "${SERVER_DIR}" && tar --exclude='.web-admin' --exclude='*.log.lck' -czf "${backupPath}" . 2>/dev/null`,
|
||||
(error) => {
|
||||
if (error) {
|
||||
console.error('Erreur création backup:', error);
|
||||
} else {
|
||||
console.log(`✓ Backup créé: ${backupName}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur création backup:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/delete/:filename', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
|
||||
if (filename.includes('..')) {
|
||||
return res.status(400).json({ error: 'Accès non autorisé' });
|
||||
}
|
||||
|
||||
const backupPath = path.join(SERVER_DIR, 'backups', filename);
|
||||
|
||||
if (!await fs.pathExists(backupPath)) {
|
||||
return res.status(404).json({ error: 'Fichier non trouvé' });
|
||||
}
|
||||
|
||||
await fs.remove(backupPath);
|
||||
res.json({ message: 'Backup supprimé' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression backup:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
138
backend/src/routes/logs.js
Normal file
138
backend/src/routes/logs.js
Normal file
@@ -0,0 +1,138 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { lines = 100 } = req.query;
|
||||
const logsDir = SERVER_DIR;
|
||||
|
||||
let logFile = path.join(logsDir, 'latest.log');
|
||||
|
||||
if (!await fs.pathExists(logFile)) {
|
||||
logFile = path.join(logsDir, 'ForgeModLoader-server-0.log');
|
||||
}
|
||||
|
||||
if (!await fs.pathExists(logFile)) {
|
||||
return res.json({ logs: [], message: 'Aucun fichier log trouvé' });
|
||||
}
|
||||
|
||||
const content = await fs.readFile(logFile, 'utf-8');
|
||||
const logLines = content.split('\n');
|
||||
const lastLines = logLines.slice(-parseInt(lines));
|
||||
|
||||
res.json({
|
||||
logs: lastLines,
|
||||
file: path.basename(logFile),
|
||||
totalLines: logLines.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur logs:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/files', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const logsDir = SERVER_DIR;
|
||||
const files = await fs.readdir(logsDir);
|
||||
|
||||
const logFiles = files.filter(f =>
|
||||
f.includes('log') || f.includes('Log')
|
||||
);
|
||||
|
||||
const filesWithStats = await Promise.all(
|
||||
logFiles.map(async (f) => {
|
||||
const stats = await fs.stat(path.join(logsDir, f));
|
||||
return {
|
||||
name: f,
|
||||
size: (stats.size / 1024).toFixed(2) + ' KB',
|
||||
modified: stats.mtime
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json(filesWithStats.sort((a, b) => new Date(b.modified) - new Date(a.modified)));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur liste logs:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/file/:filename', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
const { lines = 100 } = req.query;
|
||||
|
||||
if (filename.includes('..')) {
|
||||
return res.status(400).json({ error: 'Accès non autorisé' });
|
||||
}
|
||||
|
||||
const filePath = path.join(SERVER_DIR, filename);
|
||||
|
||||
if (!await fs.pathExists(filePath)) {
|
||||
return res.status(404).json({ error: 'Fichier non trouvé' });
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const fileLines = content.split('\n');
|
||||
const lastLines = fileLines.slice(-parseInt(lines));
|
||||
|
||||
res.json({
|
||||
logs: lastLines,
|
||||
file: filename,
|
||||
totalLines: fileLines.length
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lecture log:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/search', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { query } = req.query;
|
||||
|
||||
if (!query || query.length < 2) {
|
||||
return res.status(400).json({ error: 'Requête trop courte' });
|
||||
}
|
||||
|
||||
let logFile = path.join(SERVER_DIR, 'latest.log');
|
||||
if (!await fs.pathExists(logFile)) {
|
||||
logFile = path.join(SERVER_DIR, 'ForgeModLoader-server-0.log');
|
||||
}
|
||||
|
||||
if (!await fs.pathExists(logFile)) {
|
||||
return res.json({ results: [] });
|
||||
}
|
||||
|
||||
const content = await fs.readFile(logFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const results = lines
|
||||
.map((line, index) => ({ line, index }))
|
||||
.filter(({ line }) => line.toLowerCase().includes(query.toLowerCase()))
|
||||
.slice(-50);
|
||||
|
||||
res.json({ results });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur recherche:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
62
backend/src/routes/players.js
Normal file
62
backend/src/routes/players.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const playersDir = path.join(SERVER_DIR, 'playerdata');
|
||||
const statsDir = path.join(SERVER_DIR, 'stats');
|
||||
|
||||
if (!await fs.pathExists(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));
|
||||
|
||||
// Récupérer les noms des joueurs
|
||||
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;
|
||||
}, {});
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture usercache:', e);
|
||||
}
|
||||
}
|
||||
|
||||
const players = playerFiles.map(file => {
|
||||
const uuid = file.replace('.dat', '');
|
||||
return {
|
||||
uuid,
|
||||
name: usercache[uuid] || 'Inconnu',
|
||||
lastPlayed: new Date()
|
||||
};
|
||||
});
|
||||
|
||||
res.json({ players });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur joueurs:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
183
backend/src/routes/rcon.js
Normal file
183
backend/src/routes/rcon.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const express = require('express');
|
||||
const RconClient = require('../utils/rcon');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
async function getRconConfig() {
|
||||
const serverPropsFile = path.join(SERVER_DIR, 'server.properties');
|
||||
try {
|
||||
const content = await fs.readFile(serverPropsFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
let config = { host: 'localhost', port: 25575, password: '' };
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('rcon.port=')) {
|
||||
config.port = parseInt(line.split('=')[1].trim());
|
||||
} else if (line.startsWith('rcon.password=')) {
|
||||
config.password = line.split('=')[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error('Erreur lecture config RCON:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function addToHistory(command, response) {
|
||||
try {
|
||||
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
|
||||
await fs.ensureDir(path.dirname(historyFile));
|
||||
|
||||
let history = [];
|
||||
if (await fs.pathExists(historyFile)) {
|
||||
history = await fs.readJson(historyFile);
|
||||
}
|
||||
|
||||
history.push({
|
||||
timestamp: new Date(),
|
||||
command,
|
||||
response: response.substring(0, 500)
|
||||
});
|
||||
|
||||
await fs.writeJson(historyFile, history.slice(-100), { spaces: 2 });
|
||||
} catch (e) {
|
||||
console.error('Erreur sauvegarde historique:', e);
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/command', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { command } = req.body;
|
||||
|
||||
if (!command) {
|
||||
return res.status(400).json({ error: 'Commande manquante' });
|
||||
}
|
||||
|
||||
const config = await getRconConfig();
|
||||
if (!config || !config.password) {
|
||||
return res.status(500).json({ error: 'Configuration RCON non trouvée' });
|
||||
}
|
||||
|
||||
const rcon = new RconClient(config.host, config.port, config.password);
|
||||
|
||||
try {
|
||||
await rcon.connect();
|
||||
const response = await rcon.sendCommand(command);
|
||||
rcon.disconnect();
|
||||
|
||||
await addToHistory(command, response);
|
||||
|
||||
res.json({ response, command });
|
||||
} catch (e) {
|
||||
console.error('Erreur RCON:', e);
|
||||
rcon.disconnect();
|
||||
res.status(500).json({ error: `Erreur RCON: ${e.message}` });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur commande:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/history', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const historyFile = path.join(SERVER_DIR, '.web-admin/rcon-history.json');
|
||||
|
||||
if (await fs.pathExists(historyFile)) {
|
||||
const history = await fs.readJson(historyFile);
|
||||
res.json(history);
|
||||
} else {
|
||||
res.json([]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur historique:', e);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/restart', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const config = await getRconConfig();
|
||||
if (!config || !config.password) {
|
||||
return res.status(500).json({ error: 'Configuration RCON non trouvée' });
|
||||
}
|
||||
|
||||
const rcon = new RconClient(config.host, config.port, config.password);
|
||||
|
||||
try {
|
||||
await rcon.connect();
|
||||
|
||||
await rcon.sendCommand('say §6[ADMIN] Serveur redémarrage dans 30 secondes!');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
await rcon.sendCommand('say §6[ADMIN] 25 secondes...');
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
await rcon.sendCommand('say §6[ADMIN] 20 secondes...');
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
await rcon.sendCommand('say §6[ADMIN] Redémarrage maintenant!');
|
||||
|
||||
const response = await rcon.sendCommand('stop');
|
||||
rcon.disconnect();
|
||||
|
||||
await addToHistory('stop', response);
|
||||
|
||||
res.json({ message: 'Redémarrage en cours...' });
|
||||
} catch (e) {
|
||||
console.error('Erreur redémarrage:', e);
|
||||
rcon.disconnect();
|
||||
res.status(500).json({ error: `Erreur: ${e.message}` });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/save', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const config = await getRconConfig();
|
||||
if (!config || !config.password) {
|
||||
return res.status(500).json({ error: 'Configuration RCON non trouvée' });
|
||||
}
|
||||
|
||||
const rcon = new RconClient(config.host, config.port, config.password);
|
||||
|
||||
try {
|
||||
await rcon.connect();
|
||||
await rcon.sendCommand('say §6[ADMIN] Sauvegarde en cours...');
|
||||
const r1 = await rcon.sendCommand('save-off');
|
||||
const r2 = await rcon.sendCommand('save-all');
|
||||
const r3 = await rcon.sendCommand('save-on');
|
||||
await rcon.sendCommand('say §6[ADMIN] Sauvegarde terminée!');
|
||||
rcon.disconnect();
|
||||
|
||||
await addToHistory('save-all', r2);
|
||||
|
||||
res.json({ message: 'Sauvegarde effectuée' });
|
||||
} catch (e) {
|
||||
console.error('Erreur sauvegarde:', e);
|
||||
rcon.disconnect();
|
||||
res.status(500).json({ error: `Erreur: ${e.message}` });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
79
backend/src/routes/server.js
Normal file
79
backend/src/routes/server.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const serverPropsFile = path.join(SERVER_DIR, 'server.properties');
|
||||
|
||||
if (!await fs.pathExists(serverPropsFile)) {
|
||||
return res.status(404).json({ error: 'server.properties non trouvé' });
|
||||
}
|
||||
|
||||
const content = await fs.readFile(serverPropsFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const properties = {};
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line && !line.startsWith('#')) {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
properties[key.trim()] = valueParts.join('=').trim();
|
||||
}
|
||||
});
|
||||
|
||||
res.json({ properties });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur serveur:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/update', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { property, value } = req.body;
|
||||
|
||||
if (!property || value === undefined) {
|
||||
return res.status(400).json({ error: 'Données manquantes' });
|
||||
}
|
||||
|
||||
const serverPropsFile = path.join(SERVER_DIR, 'server.properties');
|
||||
let content = await fs.readFile(serverPropsFile, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let found = false;
|
||||
const newContent = lines.map(line => {
|
||||
if (line.startsWith(property + '=')) {
|
||||
found = true;
|
||||
return `${property}=${value}`;
|
||||
}
|
||||
return line;
|
||||
}).join('\n');
|
||||
|
||||
if (!found) {
|
||||
content = newContent + '\n' + property + '=' + value;
|
||||
} else {
|
||||
content = newContent;
|
||||
}
|
||||
|
||||
await fs.writeFile(serverPropsFile, content, 'utf-8');
|
||||
res.json({ message: 'Propriété mise à jour' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur mise à jour:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
123
backend/src/routes/whitelist.js
Normal file
123
backend/src/routes/whitelist.js
Normal file
@@ -0,0 +1,123 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { SERVER_DIR } = require('../server');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function isAuthenticated(req, res, next) {
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ error: 'Non authentifié' });
|
||||
}
|
||||
}
|
||||
|
||||
async function getWhitelistFile() {
|
||||
const jsonFile = path.join(SERVER_DIR, 'whitelist.json');
|
||||
const txtFile = path.join(SERVER_DIR, 'whitelist.txt');
|
||||
|
||||
if (await fs.pathExists(jsonFile)) {
|
||||
return jsonFile;
|
||||
} else if (await fs.pathExists(txtFile)) {
|
||||
return txtFile;
|
||||
}
|
||||
return jsonFile;
|
||||
}
|
||||
|
||||
router.get('/', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const whitelistFile = await getWhitelistFile();
|
||||
|
||||
if (!await fs.pathExists(whitelistFile)) {
|
||||
return res.json({ whitelist: [], format: 'json' });
|
||||
}
|
||||
|
||||
if (whitelistFile.endsWith('.json')) {
|
||||
const whitelist = await fs.readJson(whitelistFile);
|
||||
res.json({ whitelist, format: 'json' });
|
||||
} else {
|
||||
const content = await fs.readFile(whitelistFile, 'utf-8');
|
||||
const whitelist = content.split('\n').filter(line => line.trim());
|
||||
res.json({ whitelist, format: 'txt' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur whitelist:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/add', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { username } = req.body;
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Nom manquant' });
|
||||
}
|
||||
|
||||
const whitelistFile = await getWhitelistFile();
|
||||
|
||||
if (whitelistFile.endsWith('.json')) {
|
||||
let whitelist = [];
|
||||
if (await fs.pathExists(whitelistFile)) {
|
||||
whitelist = await fs.readJson(whitelistFile);
|
||||
}
|
||||
|
||||
if (whitelist.some(w => (typeof w === 'string' ? w : w.name) === username)) {
|
||||
return res.status(400).json({ error: 'Joueur déjà en whitelist' });
|
||||
}
|
||||
|
||||
whitelist.push({ name: username, uuid: '' });
|
||||
await fs.writeJson(whitelistFile, whitelist, { spaces: 2 });
|
||||
} else {
|
||||
let content = '';
|
||||
if (await fs.pathExists(whitelistFile)) {
|
||||
content = await fs.readFile(whitelistFile, 'utf-8');
|
||||
}
|
||||
|
||||
if (content.includes(username)) {
|
||||
return res.status(400).json({ error: 'Joueur déjà en whitelist' });
|
||||
}
|
||||
|
||||
content += (content ? '\n' : '') + username;
|
||||
await fs.writeFile(whitelistFile, content, 'utf-8');
|
||||
}
|
||||
|
||||
res.json({ message: 'Joueur ajouté à la whitelist' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur ajout whitelist:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/remove', isAuthenticated, async (req, res) => {
|
||||
try {
|
||||
const { username } = req.body;
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).json({ error: 'Nom manquant' });
|
||||
}
|
||||
|
||||
const whitelistFile = await getWhitelistFile();
|
||||
|
||||
if (whitelistFile.endsWith('.json')) {
|
||||
let whitelist = await fs.readJson(whitelistFile);
|
||||
whitelist = whitelist.filter(w => (typeof w === 'string' ? w : w.name) !== username);
|
||||
await fs.writeJson(whitelistFile, whitelist, { spaces: 2 });
|
||||
} else {
|
||||
let content = await fs.readFile(whitelistFile, 'utf-8');
|
||||
content = content.split('\n').filter(line => line.trim() !== username).join('\n');
|
||||
await fs.writeFile(whitelistFile, content, 'utf-8');
|
||||
}
|
||||
|
||||
res.json({ message: 'Joueur retiré de la whitelist' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur suppression whitelist:', error);
|
||||
res.status(500).json({ error: 'Erreur serveur' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
73
backend/src/server.js
Normal file
73
backend/src/server.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const path = require('path');
|
||||
const dotenv = require('dotenv');
|
||||
const cors = require('cors');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
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';
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cors({
|
||||
origin: process.env.NODE_ENV === 'production' ? false : true,
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Session config
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-prod',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 heures
|
||||
}
|
||||
}));
|
||||
|
||||
// Importation des routes
|
||||
const authRoutes = require('./routes/auth');
|
||||
const serverRoutes = require('./routes/server');
|
||||
const rconRoutes = require('./routes/rcon');
|
||||
const logsRoutes = require('./routes/logs');
|
||||
const playersRoutes = require('./routes/players');
|
||||
const whitelistRoutes = require('./routes/whitelist');
|
||||
const backupRoutes = require('./routes/backup');
|
||||
|
||||
// Routes API
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/server', serverRoutes);
|
||||
app.use('/api/rcon', rconRoutes);
|
||||
app.use('/api/logs', logsRoutes);
|
||||
app.use('/api/players', playersRoutes);
|
||||
app.use('/api/whitelist', whitelistRoutes);
|
||||
app.use('/api/backup', backupRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date() });
|
||||
});
|
||||
|
||||
// Erreur 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Route non trouvée' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: 'Erreur serveur interne' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n🚀 Backend Admin NationsGlory démarré sur http://localhost:${PORT}`);
|
||||
console.log(`📁 Répertoire du serveur: ${SERVER_DIR}\n`);
|
||||
});
|
||||
|
||||
module.exports = { app, SERVER_DIR };
|
||||
141
backend/src/utils/rcon.js
Normal file
141
backend/src/utils/rcon.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const net = require('net');
|
||||
|
||||
class RconClient {
|
||||
constructor(host, port, password) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
this.socket = null;
|
||||
this.authenticated = false;
|
||||
this.packetId = 0;
|
||||
this.responseBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
createPacket(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;
|
||||
|
||||
const size = body.length;
|
||||
const packet = Buffer.alloc(4 + size);
|
||||
packet.writeInt32LE(size, 0);
|
||||
body.copy(packet, 4);
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async connect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = net.createConnection({
|
||||
host: this.host,
|
||||
port: this.port
|
||||
});
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('✓ Connecté au serveur RCON');
|
||||
this.authenticate().then(resolve).catch(reject);
|
||||
});
|
||||
|
||||
this.socket.on('error', reject);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timeout de connexion'));
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.packetId++;
|
||||
const packet = this.createPacket(3, this.password);
|
||||
|
||||
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'));
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
async sendCommand(command) {
|
||||
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.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'));
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
this.authenticated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RconClient;
|
||||
Reference in New Issue
Block a user