mirror of
https://github.com/ibratabian17/OpenParty.git
synced 2026-01-15 14:22:54 -03:00
feat: Improve Account System, Add Personalized Carousel, Improve Plugin Format
This commit is contained in:
704
plugins/AdminPanel/AdminPanelPlugin.js
Normal file
704
plugins/AdminPanel/AdminPanelPlugin.js
Normal file
@@ -0,0 +1,704 @@
|
||||
/**
|
||||
* Admin Panel Plugin for OpenParty
|
||||
* Provides a secure web interface for server management
|
||||
*/
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { exec } = require('child_process');
|
||||
const archiver = require('archiver'); // Moved to top
|
||||
const fs = require('fs');
|
||||
const path = require('path'); // Standard library
|
||||
const Plugin = require('../../core/classes/Plugin'); // Adjusted path
|
||||
const Logger = require('../../core/utils/logger'); // Adjusted path
|
||||
|
||||
class AdminPanelPlugin extends Plugin {
|
||||
constructor() {
|
||||
super('AdminPanelPlugin', 'Secure admin panel for server management');
|
||||
this.logger = new Logger('AdminPanel');
|
||||
this.sessionSecret = process.env.SESSION_SECRET || 'openparty-secure-session';
|
||||
|
||||
// Initialize admin password (logger might not be fully initialized with manifest name yet)
|
||||
const plainPassword = process.env.ADMIN_PASSWORD || 'admin123';
|
||||
console.log('[AdminPanelPlugin] Initializing admin password...'); // Use console.log before logger is guaranteed
|
||||
console.log(`[AdminPanelPlugin] Using default password: ${!process.env.ADMIN_PASSWORD}`);
|
||||
|
||||
// Hash the admin password
|
||||
try {
|
||||
this.adminPassword = bcrypt.hashSync(plainPassword, 10);
|
||||
this.logger.info('Admin password hashed successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to hash admin password:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.backupInterval = 24 * 60 * 60 * 1000; // 24 hours
|
||||
this.startTime = Date.now();
|
||||
this.stats = {
|
||||
activeUsers: 0,
|
||||
totalSongs: 0,
|
||||
lastBackup: null,
|
||||
activePlugins: 0
|
||||
};
|
||||
|
||||
this.app = null; // To store the Express app instance
|
||||
|
||||
// Initialize stats update interval
|
||||
setInterval(() => this.updateStats(), 30000); // Update every 30 seconds
|
||||
}
|
||||
|
||||
initroute(app) {
|
||||
this.app = app; // Store the Express app instance
|
||||
this.logger.info('Initializing admin panel routes...');
|
||||
|
||||
// Body parsing middleware (assuming express.json and express.urlencoded are global)
|
||||
// If not, they might need to be added here or in Core.js globally.
|
||||
// For this integration, we'll assume they are handled globally.
|
||||
|
||||
// Session middleware - specific to the admin panel
|
||||
app.use(session({
|
||||
secret: this.sessionSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production', // Only use secure cookies in production
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Serve static files
|
||||
app.use('/panel', express.static(path.join(__dirname, 'panel/public')));
|
||||
this.logger.info(`Serving static files from: ${path.join(__dirname, 'panel/public')}`);
|
||||
|
||||
// Make sure the directory exists
|
||||
if (!fs.existsSync(path.join(__dirname, 'panel/public'))) {
|
||||
this.logger.error(`Static files directory does not exist: ${path.join(__dirname, 'panel/public')}`);
|
||||
fs.mkdirSync(path.join(__dirname, 'panel/public'), { recursive: true });
|
||||
this.logger.info(`Created static files directory: ${path.join(__dirname, 'panel/public')}`);
|
||||
}
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session.authenticated) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect('/panel/login');
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Consider implementing rate limiting for login attempts to prevent brute-force attacks.
|
||||
// Example: using a middleware like 'express-rate-limit'.
|
||||
// Login route
|
||||
app.get('/panel/login', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'panel/public/login.html'));
|
||||
});
|
||||
|
||||
app.post('/panel/login', async (req, res) => {
|
||||
try {
|
||||
this.logger.info('Login attempt received');
|
||||
this.logger.info('Request body:', req.body);
|
||||
|
||||
const { password } = req.body;
|
||||
if (!password) {
|
||||
this.logger.warn('No password provided');
|
||||
return res.redirect('/panel/login?error=1');
|
||||
}
|
||||
|
||||
this.logger.info('Comparing passwords...');
|
||||
const match = await bcrypt.compare(password, this.adminPassword);
|
||||
this.logger.info(`Password match result: ${match}`);
|
||||
|
||||
if (match) {
|
||||
req.session.authenticated = true;
|
||||
this.logger.info('Login successful');
|
||||
res.redirect('/panel/dashboard');
|
||||
} else {
|
||||
this.logger.warn('Invalid password attempt');
|
||||
res.redirect('/panel/login?error=1');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Login error: ${error.message}`);
|
||||
this.logger.error(error.stack);
|
||||
res.redirect('/panel/login?error=1');
|
||||
}
|
||||
});
|
||||
|
||||
// Dashboard
|
||||
app.get('/panel/dashboard', requireAuth, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'panel/public/dashboard.html'));
|
||||
});
|
||||
|
||||
// Plugin management
|
||||
app.get('/panel/api/plugins', requireAuth, (req, res) => {
|
||||
const pluginManager = req.app.get('pluginManager');
|
||||
const pluginsMap = pluginManager.getPlugins();
|
||||
const pluginsArray = Array.from(pluginsMap.values()).map(plugin => ({
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
enabled: plugin.isEnabled()
|
||||
}));
|
||||
res.json(pluginsArray);
|
||||
});
|
||||
|
||||
app.post('/panel/api/plugins/toggle', requireAuth, (req, res) => {
|
||||
try {
|
||||
const pluginManager = req.app.get('pluginManager');
|
||||
const { pluginName } = req.body; // Changed from 'name' to 'pluginName'
|
||||
|
||||
if (!pluginName) {
|
||||
return res.status(400).json({ success: false, message: 'Plugin name (pluginName) is required' });
|
||||
}
|
||||
|
||||
const plugin = pluginManager.getPlugin(pluginName); // Use getPlugin for direct access
|
||||
|
||||
if (!plugin) {
|
||||
return res.status(404).json({ success: false, message: `Plugin ${pluginName} not found` });
|
||||
}
|
||||
|
||||
let newStatusMessage;
|
||||
if (plugin.isEnabled()) {
|
||||
plugin.disable();
|
||||
newStatusMessage = `Plugin ${pluginName} has been disabled`;
|
||||
this.logger.info(newStatusMessage);
|
||||
} else {
|
||||
plugin.enable();
|
||||
newStatusMessage = `Plugin ${pluginName} has been enabled`;
|
||||
this.logger.info(newStatusMessage);
|
||||
}
|
||||
res.json({ success: true, message: newStatusMessage, enabled: plugin.isEnabled() });
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Error toggling plugin: ${error.message}`);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Server status endpoint
|
||||
app.get('/panel/api/status', requireAuth, (req, res) => {
|
||||
const settings = require('../../settings.json');
|
||||
let currentVersion = 'N/A';
|
||||
try {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
currentVersion = packageJson.version;
|
||||
}
|
||||
} catch (e) { this.logger.error("Failed to read package.json version for status API", e); }
|
||||
res.json({
|
||||
maintenance: settings.server.serverstatus.isMaintenance,
|
||||
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
||||
version: currentVersion
|
||||
});
|
||||
});
|
||||
|
||||
// Server stats endpoint
|
||||
app.get('/panel/api/stats', requireAuth, (req, res) => {
|
||||
res.json(this.stats);
|
||||
});
|
||||
|
||||
// Server management endpoints
|
||||
app.post('/panel/api/update', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
||||
exec('git pull && npm install', (error, stdout, stderr) => {
|
||||
if (error) reject(error);
|
||||
else resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
this.logger.info('Update successful');
|
||||
res.json({ message: 'Update successful', output: stdout, details: stderr });
|
||||
} catch (error) {
|
||||
this.logger.error(`Update error: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/panel/api/restart', requireAuth, (req, res) => {
|
||||
res.json({ message: 'Server restarting...' });
|
||||
process.exit(42); // Trigger restart through PM2
|
||||
});
|
||||
|
||||
app.post('/panel/api/reload-plugins', requireAuth, (req, res) => {
|
||||
try {
|
||||
const pluginManager = req.app.get('pluginManager');
|
||||
pluginManager.loadPlugins(); // No longer needs settings.modules
|
||||
this.logger.info('Plugins reloaded via API.');
|
||||
res.json({ message: 'Plugins reloaded successfully' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Savedata management
|
||||
app.get('/panel/api/savedata', requireAuth, (req, res) => {
|
||||
try {
|
||||
const savedataPath = path.join(process.cwd(), 'database/data');
|
||||
|
||||
if (!fs.existsSync(savedataPath)) {
|
||||
this.logger.error(`Savedata directory does not exist: ${savedataPath}`);
|
||||
return res.status(404).json({ error: 'Savedata directory not found' });
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(savedataPath)
|
||||
.filter(file => file.endsWith('.json'))
|
||||
.map(file => {
|
||||
const filePath = path.join(savedataPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
name: file,
|
||||
size: stats.size,
|
||||
modified: stats.mtime
|
||||
};
|
||||
});
|
||||
|
||||
res.json(files);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting savedata: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/panel/api/savedata/:type', requireAuth, (req, res) => {
|
||||
const { type } = req.params;
|
||||
const { data } = req.body;
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
path.join(process.cwd(), 'database/data', `${type}.json`),
|
||||
JSON.stringify(data, null, 2)
|
||||
);
|
||||
// Commit changes to Git
|
||||
exec(`git add . && git commit -m "Update ${type} savedata" && git push`, {
|
||||
cwd: process.cwd()
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
this.logger.error(`Git error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
res.json({ message: 'Savedata updated successfully' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Backup system
|
||||
this.setupAutomaticBackup();
|
||||
|
||||
// Get backups list
|
||||
app.get('/panel/api/backups', requireAuth, (req, res) => {
|
||||
try {
|
||||
const backupsPath = path.join(process.cwd(), 'backups');
|
||||
|
||||
if (!fs.existsSync(backupsPath)) {
|
||||
this.logger.info(`Backups directory does not exist, creating: ${backupsPath}`);
|
||||
fs.mkdirSync(backupsPath, { recursive: true });
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const backups = fs.readdirSync(backupsPath)
|
||||
.filter(dir => {
|
||||
const dirPath = path.join(backupsPath, dir);
|
||||
return fs.statSync(dirPath).isDirectory();
|
||||
})
|
||||
.map(dir => {
|
||||
const dirPath = path.join(backupsPath, dir);
|
||||
const stats = fs.statSync(dirPath);
|
||||
|
||||
// Calculate total size of backup
|
||||
let totalSize = 0;
|
||||
const dataPath = path.join(dirPath, 'data');
|
||||
if (fs.existsSync(dataPath)) {
|
||||
const calculateDirSize = (dirPath) => {
|
||||
let size = 0;
|
||||
const files = fs.readdirSync(dirPath);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
size += calculateDirSize(filePath);
|
||||
} else {
|
||||
size += stat.size;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
};
|
||||
totalSize = calculateDirSize(dataPath);
|
||||
}
|
||||
|
||||
return {
|
||||
filename: dir,
|
||||
date: new Date(dir.replace(/-/g, ':')),
|
||||
size: totalSize,
|
||||
type: 'Auto'
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by date (newest first)
|
||||
backups.sort((a, b) => b.date - a.date);
|
||||
|
||||
res.json(backups);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting backups: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create backup
|
||||
app.post('/panel/api/backup', requireAuth, (req, res) => {
|
||||
this.createBackup()
|
||||
.then(() => res.json({ success: true, message: 'Backup created successfully' }))
|
||||
.catch(error => res.status(500).json({ success: false, error: error.message }));
|
||||
});
|
||||
|
||||
// Download backup
|
||||
app.get('/panel/api/backups/download/:filename', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
if (!filename) {
|
||||
return res.status(400).json({ success: false, message: 'Backup filename is required' });
|
||||
}
|
||||
|
||||
const backupPath = path.join(process.cwd(), 'backups', filename);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
this.logger.error(`Backup not found: ${backupPath}`);
|
||||
return res.status(404).json({ success: false, message: 'Backup not found' });
|
||||
}
|
||||
|
||||
// Create a zip file of the backup
|
||||
const zipFilename = `${filename}.zip`;
|
||||
const zipPath = path.join(process.cwd(), 'backups', zipFilename);
|
||||
|
||||
// Create a write stream for the zip file
|
||||
const output = fs.createWriteStream(zipPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 } // Maximum compression
|
||||
});
|
||||
|
||||
// Listen for all archive data to be written
|
||||
output.on('close', () => {
|
||||
this.logger.info(`Backup archive created: ${zipPath} (${archive.pointer()} bytes)`);
|
||||
|
||||
// Send the zip file
|
||||
res.download(zipPath, zipFilename, (err) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error sending backup: ${err.message}`);
|
||||
}
|
||||
|
||||
// Delete the temporary zip file after sending
|
||||
fs.unlink(zipPath, (unlinkErr) => {
|
||||
if (unlinkErr) {
|
||||
this.logger.error(`Error deleting temporary zip file: ${unlinkErr.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
archive.on('error', (err) => {
|
||||
this.logger.error(`Error creating backup archive: ${err.message}`);
|
||||
res.status(500).json({ success: false, message: `Error creating backup archive: ${err.message}` });
|
||||
});
|
||||
|
||||
// Pipe archive data to the output file
|
||||
archive.pipe(output);
|
||||
|
||||
// Add the backup directory to the archive
|
||||
archive.directory(backupPath, false);
|
||||
|
||||
// Finalize the archive
|
||||
archive.finalize();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error downloading backup: ${error.message}`);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete backup
|
||||
app.delete('/panel/api/backups/delete/:filename', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { filename } = req.params;
|
||||
if (!filename) {
|
||||
return res.status(400).json({ success: false, message: 'Backup filename is required' });
|
||||
}
|
||||
|
||||
const backupPath = path.join(process.cwd(), 'backups', filename);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
this.logger.error(`Backup not found: ${backupPath}`);
|
||||
return res.status(404).json({ success: false, message: 'Backup not found' });
|
||||
}
|
||||
|
||||
// Delete the backup directory recursively
|
||||
fs.rmSync(backupPath, { recursive: true, force: true });
|
||||
|
||||
this.logger.info(`Backup deleted: ${filename}`);
|
||||
res.json({ success: true, message: 'Backup deleted successfully' });
|
||||
} catch (error) {
|
||||
this.logger.error(`Error deleting backup: ${error.message}`);
|
||||
res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check for updates
|
||||
app.get('/panel/api/check-updates', requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Get current version from package.json
|
||||
const packagePath = path.join(process.cwd(), 'package.json');
|
||||
let currentVersion = '1.0.0';
|
||||
|
||||
if (fs.existsSync(packagePath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
currentVersion = packageJson.version || '1.0.0';
|
||||
}
|
||||
|
||||
// For demo purposes, we'll simulate checking for updates
|
||||
// In a real implementation, you would fetch from a remote repository
|
||||
const hasUpdate = Math.random() > 0.5; // Randomly determine if update is available
|
||||
|
||||
if (hasUpdate) {
|
||||
// Simulate a newer version
|
||||
const newVersion = currentVersion.split('.')
|
||||
.map((part, index) => index === 2 ? parseInt(part) + 1 : part)
|
||||
.join('.');
|
||||
|
||||
res.json({
|
||||
available: true,
|
||||
currentVersion,
|
||||
version: newVersion,
|
||||
changelog: 'Bug fixes and performance improvements.'
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
available: false,
|
||||
currentVersion
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error checking for updates: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Maintenance mode
|
||||
app.post('/panel/api/maintenance', requireAuth, (req, res) => {
|
||||
try {
|
||||
const settingsPath = path.join(process.cwd(), 'settings.json');
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
|
||||
// Toggle maintenance mode
|
||||
settings.server.serverstatus.isMaintenance = !settings.server.serverstatus.isMaintenance;
|
||||
|
||||
// Write updated settings back to file
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(settings, null, 2)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
enabled: settings.server.serverstatus.isMaintenance
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error toggling maintenance mode: ${error.message}`);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Settings endpoint
|
||||
app.get('/panel/api/settings', requireAuth, (req, res) => {
|
||||
try {
|
||||
const settingsPath = path.join(process.cwd(), 'settings.json'); // process.cwd() is the root
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
|
||||
// Return a sanitized version of settings (remove sensitive data if needed)
|
||||
res.json({
|
||||
server: {
|
||||
port: settings.server.port,
|
||||
isPublic: settings.server.isPublic,
|
||||
enableSSL: settings.server.enableSSL,
|
||||
domain: settings.server.domain,
|
||||
modName: settings.server.modName,
|
||||
maintenance: settings.server.serverstatus.isMaintenance,
|
||||
channel: settings.server.serverstatus.channel
|
||||
},
|
||||
// Get plugin info from PluginManager and manifests
|
||||
plugins: [] // Placeholder, will be populated below
|
||||
});
|
||||
const pluginManager = req.app.get('pluginManager');
|
||||
if (pluginManager) {
|
||||
const pluginsMap = pluginManager.getPlugins();
|
||||
res.locals.plugins = Array.from(pluginsMap.values()).map(p => ({
|
||||
name: p.manifest.name,
|
||||
description: p.manifest.description,
|
||||
execution: p.manifest.execution,
|
||||
enabled: p.isEnabled(),
|
||||
version: p.manifest.version
|
||||
}));
|
||||
}
|
||||
res.json(res.locals); // Send the combined data
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting settings: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/panel/api/settings', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { server } = req.body;
|
||||
|
||||
if (!server) {
|
||||
return res.status(400).json({ success: false, message: 'Server settings are required' });
|
||||
}
|
||||
|
||||
const settingsPath = path.join(process.cwd(), 'settings.json');
|
||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||
|
||||
// Update only allowed settings
|
||||
if (server.port !== undefined) settings.server.port = server.port;
|
||||
if (server.isPublic !== undefined) settings.server.isPublic = server.isPublic;
|
||||
if (server.enableSSL !== undefined) settings.server.enableSSL = server.enableSSL;
|
||||
if (server.domain !== undefined) settings.server.domain = server.domain;
|
||||
if (server.modName !== undefined) settings.server.modName = server.modName;
|
||||
if (server.maintenance !== undefined) settings.server.serverstatus.isMaintenance = server.maintenance;
|
||||
if (server.channel !== undefined) settings.server.serverstatus.channel = server.channel;
|
||||
|
||||
// Write updated settings back to file
|
||||
fs.writeFileSync(
|
||||
settingsPath,
|
||||
JSON.stringify(settings, null, 2)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Settings updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error updating settings: ${error.message}`);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Logs endpoint
|
||||
app.get('/panel/api/logs', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { level = 'all', limit = 100 } = req.query;
|
||||
const logsDir = path.join(process.cwd(), 'logs');
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// For demo purposes, generate some sample logs if no log file exists
|
||||
const logFile = path.join(logsDir, 'server.log');
|
||||
if (!fs.existsSync(logFile)) {
|
||||
const sampleLogs = [
|
||||
'[INFO] Server started successfully',
|
||||
'[INFO] Loaded 3 plugins',
|
||||
'[WARNING] Plugin XYZ is using deprecated API',
|
||||
'[ERROR] Failed to connect to database',
|
||||
'[INFO] User logged in: admin',
|
||||
'[DEBUG] Processing request: GET /api/songs',
|
||||
'[INFO] Request completed in 120ms'
|
||||
];
|
||||
fs.writeFileSync(logFile, sampleLogs.join('\n'));
|
||||
}
|
||||
|
||||
// Read log file
|
||||
let logs = fs.readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
|
||||
|
||||
// Filter by level if specified
|
||||
if (level !== 'all') {
|
||||
const levelUpper = level.toUpperCase();
|
||||
logs = logs.filter(log => log.includes(`[${levelUpper}]`));
|
||||
}
|
||||
|
||||
// Limit number of logs
|
||||
logs = logs.slice(-parseInt(limit));
|
||||
|
||||
res.json({
|
||||
logs,
|
||||
total: logs.length
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Error getting logs: ${error.message}`);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.info('Admin panel routes initialized');
|
||||
}
|
||||
|
||||
async updateStats() {
|
||||
try {
|
||||
// Update active users (example: count connected clients)
|
||||
this.stats.activeUsers = Object.keys(this.app?.io?.sockets?.sockets || global.io?.sockets?.sockets || {}).length; // Prefer app.io if available
|
||||
|
||||
// Update total songs
|
||||
const songPath = path.join(process.cwd(), 'database/data/songs.json');
|
||||
if (fs.existsSync(songPath)) {
|
||||
const songs = JSON.parse(fs.readFileSync(songPath, 'utf8'));
|
||||
this.stats.totalSongs = Object.keys(songs).length;
|
||||
} else { this.stats.totalSongs = 0; }
|
||||
|
||||
// Update active plugins count
|
||||
const pluginManager = this.app?.get('pluginManager');
|
||||
if (pluginManager) {
|
||||
this.stats.activePlugins = Array.from(pluginManager.getPlugins().values()).filter(p => p.isEnabled()).length;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Stats update error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async createBackup() {
|
||||
try {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupDir = path.join(process.cwd(), 'backups', timestamp);
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
|
||||
// Backup database and savedata
|
||||
const dataDir = path.join(process.cwd(), 'database/data');
|
||||
fs.cpSync(dataDir, path.join(backupDir, 'data'), { recursive: true });
|
||||
|
||||
// Create Git tag for the backup
|
||||
await new Promise((resolve, reject) => {
|
||||
exec(`git tag backup-${timestamp} && git push origin backup-${timestamp}`, {
|
||||
cwd: process.cwd()
|
||||
}, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
this.logger.error(`Git tagging/pushing error during backup: ${error.message}`);
|
||||
this.logger.error(`Git stderr: ${stderr}`);
|
||||
reject(error);
|
||||
} else {
|
||||
this.logger.info(`Git tag and push successful for backup-${timestamp}`);
|
||||
this.logger.info(`Git stdout: ${stdout}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
// Update last backup timestamp
|
||||
this.stats.lastBackup = timestamp;
|
||||
this.logger.info(`Backup created successfully: ${timestamp}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Backup creation failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
setupAutomaticBackup() {
|
||||
setInterval(() => {
|
||||
this.createBackup().catch(error => {
|
||||
this.logger.error(`Automatic backup failed: ${error.message}`);
|
||||
});
|
||||
}, this.backupInterval);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AdminPanelPlugin();
|
||||
71
plugins/AdminPanel/panel/README.md
Normal file
71
plugins/AdminPanel/panel/README.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# OpenParty Admin Panel Plugin
|
||||
|
||||
## Overview
|
||||
Secure web-based administration interface for OpenParty server management. Features a modern dark theme UI and comprehensive server management capabilities.
|
||||
|
||||
## Features
|
||||
- Secure session-based authentication
|
||||
- Server status monitoring
|
||||
- Plugin management
|
||||
- Savedata modification and Git integration
|
||||
- Automated backups
|
||||
- Update management
|
||||
- Maintenance mode control
|
||||
- Server logs viewer
|
||||
|
||||
## Installation
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
cd plugins/panel
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Add the plugin to `settings.json`:
|
||||
```json
|
||||
{
|
||||
"modules": [
|
||||
{
|
||||
"name": "AdminPanelPlugin",
|
||||
"description": "Secure admin panel for server management",
|
||||
"path": "{dirname}/plugins/AdminPanelPlugin.js",
|
||||
"execution": "init"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
3. Set environment variables (optional):
|
||||
- `SESSION_SECRET`: Custom session secret (default: 'openparty-secure-session')
|
||||
- `ADMIN_PASSWORD`: Admin password (default: 'admin123')
|
||||
|
||||
## Security
|
||||
- Session-based authentication
|
||||
- Password hashing with bcrypt
|
||||
- HTTPS-only cookie security
|
||||
- Session expiration
|
||||
|
||||
## Usage
|
||||
1. Access the panel at: `https://your-server/panel`
|
||||
2. Login with admin credentials
|
||||
3. Use the dashboard to manage:
|
||||
- Server status and statistics
|
||||
- Plugin management
|
||||
- Savedata modifications
|
||||
- Backup management
|
||||
- Server updates
|
||||
- Maintenance mode
|
||||
- Server logs
|
||||
|
||||
## Automated Features
|
||||
- Daily automated backups
|
||||
- Git integration for savedata changes
|
||||
- Plugin hot-reloading
|
||||
- Server update management
|
||||
|
||||
## Development
|
||||
The panel uses a modern tech stack:
|
||||
- Express.js for backend
|
||||
- SQLite for session storage
|
||||
- Modern CSS with dark theme
|
||||
- Responsive design
|
||||
- Vanilla JavaScript for frontend interactions
|
||||
11
plugins/AdminPanel/panel/package.json
Normal file
11
plugins/AdminPanel/panel/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "openparty-admin-panel",
|
||||
"version": "1.0.0",
|
||||
"description": "Admin panel plugin for OpenParty server",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-sqlite3": "^0.9.13"
|
||||
}
|
||||
}
|
||||
1122
plugins/AdminPanel/panel/public/dashboard.html
Normal file
1122
plugins/AdminPanel/panel/public/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
67
plugins/AdminPanel/panel/public/js/plugins.js
Normal file
67
plugins/AdminPanel/panel/public/js/plugins.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// Plugin management functionality
|
||||
async function loadPlugins() {
|
||||
try {
|
||||
const response = await fetch('/panel/api/plugins');
|
||||
const plugins = await response.json();
|
||||
const pluginsList = document.getElementById('pluginsList');
|
||||
pluginsList.innerHTML = plugins.map(plugin => `
|
||||
<tr class="plugin-row ${plugin.enabled ? 'plugin-enabled' : 'plugin-disabled'}">
|
||||
<td class="plugin-name">${plugin.name}</td>
|
||||
<td class="plugin-description">${plugin.description || 'No description available.'}</td>
|
||||
<td class="plugin-status">
|
||||
<span class="badge ${plugin.enabled ? 'bg-success' : 'bg-secondary'}">
|
||||
<!-- Example using Font Awesome icons: replace with your icon library or remove if not using icons -->
|
||||
<!-- <i class="fas ${plugin.enabled ? 'fa-check-circle' : 'fa-times-circle'}"></i> -->
|
||||
${plugin.enabled ? 'Enabled' : 'Disabled'}
|
||||
</span>
|
||||
</td>
|
||||
<td class="plugin-actions">
|
||||
<button
|
||||
class="btn btn-sm ${plugin.enabled ? 'btn-outline-warning' : 'btn-outline-success'}"
|
||||
onclick="togglePlugin('${plugin.name}')"
|
||||
title="${plugin.enabled ? 'Disable' : 'Enable'} ${plugin.name}">
|
||||
<!-- Example using Font Awesome icons: replace with your icon library or remove if not using icons -->
|
||||
<!-- <i class="fas ${plugin.enabled ? 'fa-toggle-off' : 'fa-toggle-on'}"></i> -->
|
||||
${plugin.enabled ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
/*
|
||||
Suggested CSS for the new classes (add to your panel's CSS file):
|
||||
.plugin-row.plugin-disabled { opacity: 0.7; }
|
||||
.plugin-name { font-weight: bold; }
|
||||
.badge { padding: 0.4em 0.6em; font-size: 0.9em; }
|
||||
.bg-success { background-color: #28a745; color: white; }
|
||||
.bg-secondary { background-color: #6c757d; color: white; }
|
||||
.btn-sm { padding: 0.25rem 0.5rem; font-size: .875rem; }
|
||||
.btn-outline-warning { border-color: #ffc107; color: #ffc107; }
|
||||
.btn-outline-warning:hover { background-color: #ffc107; color: #212529; }
|
||||
.btn-outline-success { border-color: #28a745; color: #28a745; }
|
||||
.btn-outline-success:hover { background-color: #28a745; color: white; }
|
||||
*/
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePlugin(pluginName) {
|
||||
try {
|
||||
const response = await fetch('/panel/api/plugins/toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ pluginName })
|
||||
});
|
||||
const result = await response.json();
|
||||
showToast(result.message);
|
||||
loadPlugins(); // Refresh the plugins list
|
||||
} catch (error) {
|
||||
showToast(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load plugins when the plugins section is shown
|
||||
document.querySelector('[onclick="showSection(\'plugins\')"]')
|
||||
.addEventListener('click', loadPlugins);
|
||||
100
plugins/AdminPanel/panel/public/login.html
Normal file
100
plugins/AdminPanel/panel/public/login.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenParty Admin Panel - Login</title>
|
||||
<!-- Tailwind CSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Google Fonts: Inter -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#0f172a', // Dark blue/slate
|
||||
secondary: '#1e293b', // Lighter blue/slate
|
||||
tertiary: '#334155', // Even lighter blue/slate
|
||||
accent: '#3b82f6', // Bright blue
|
||||
success: '#10b981', // Green
|
||||
warning: '#f59e0b', // Amber
|
||||
error: '#ef4444', // Red
|
||||
info: '#06b6d4' // Cyan
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
mono: ['Fira Code', 'monospace']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Custom animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fadeIn {
|
||||
animation: fadeIn 0.5s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-primary min-h-screen flex items-center justify-center p-4 font-sans text-white">
|
||||
<div class="animate-fadeIn bg-secondary rounded-lg shadow-xl w-full max-w-md p-8 border border-tertiary">
|
||||
<div class="text-center mb-8">
|
||||
<i class="fas fa-lock text-accent text-4xl mb-4"></i>
|
||||
<h1 class="text-2xl font-bold mb-2">OpenParty Admin Panel</h1>
|
||||
<p class="text-gray-400">Enter your password to continue</p>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6" action="/panel/login" method="POST">
|
||||
<div class="space-y-2">
|
||||
<label for="password" class="block text-sm font-medium text-gray-400">Password</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<i class="fas fa-key text-gray-500"></i>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
class="bg-tertiary border border-gray-700 text-white pl-10 block w-full rounded-md py-3 px-4 focus:ring-2 focus:ring-accent focus:border-accent focus:outline-none transition-all duration-200"
|
||||
placeholder="Enter admin password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-white bg-accent hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent transition-all duration-200"
|
||||
>
|
||||
<i class="fas fa-sign-in-alt mr-2"></i> Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div id="error-message" class="mt-4 text-center text-error hidden">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
||||
Invalid password. Please try again.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check for error parameter in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('error') === '1') {
|
||||
document.getElementById('error-message').classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
528
plugins/FakeWDF/FakeWdfPlugin.js
Normal file
528
plugins/FakeWDF/FakeWdfPlugin.js
Normal file
@@ -0,0 +1,528 @@
|
||||
/**
|
||||
* WDF Plugin for OpenParty
|
||||
* Handles World Dance Floor (WDF) related routes as a plugin
|
||||
*/
|
||||
const axios = require('axios');
|
||||
const Plugin = require('../../core/classes/Plugin'); // Assuming Plugin is located at ../core/classes/Plugin.js
|
||||
const Logger = require('../../core/utils/logger');
|
||||
|
||||
class WDFPlugin extends Plugin {
|
||||
/**
|
||||
* Create a new WDF plugin
|
||||
*/
|
||||
constructor() {
|
||||
super('WDFPlugin', 'Handles World Dance Floor (WDF) related routes for Just Dance.');
|
||||
this.logger = new Logger('WDFPlugin');
|
||||
|
||||
// Bind handler methods to maintain 'this' context.
|
||||
// This is crucial when these methods are passed as callbacks to Express routes.
|
||||
this.handleAssignRoom = this.handleAssignRoom.bind(this);
|
||||
this.handleServerTime = this.handleServerTime.bind(this);
|
||||
this.handleScreens = this.handleScreens.bind(this);
|
||||
this.handleNewsfeed = this.handleNewsfeed.bind(this);
|
||||
this.handleOnlineBosses = this.handleOnlineBosses.bind(this);
|
||||
this.handleNextHappyHours = this.handleNextHappyHours.bind(this);
|
||||
this.handleGetNotification = this.handleGetNotification.bind(this);
|
||||
this.handlePostNotification = this.handlePostNotification.bind(this);
|
||||
this.handleGetSessionRecap = this.handleGetSessionRecap.bind(this);
|
||||
this.handlePostSessionRecap = this.handlePostSessionRecap.bind(this);
|
||||
this.handleGetScoreRecap = this.handleGetScoreRecap.bind(this);
|
||||
this.handlePostScoreRecap = this.handlePostScoreRecap.bind(this);
|
||||
this.handleGetOnlineRankWidget = this.handleGetOnlineRankWidget.bind(this);
|
||||
this.handlePostOnlineRankWidget = this.handlePostOnlineRankWidget.bind(this);
|
||||
this.handleGetSession = this.handleGetSession.bind(this);
|
||||
this.handlePostSession = this.handlePostSession.bind(this);
|
||||
this.handleGetCcu = this.handleGetCcu.bind(this);
|
||||
this.handleDeleteSession = this.handleDeleteSession.bind(this);
|
||||
this.handleGetTournamentScoreRecap = this.handleGetTournamentScoreRecap.bind(this);
|
||||
this.handlePostTournamentScoreRecap = this.handlePostTournamentScoreRecap.bind(this);
|
||||
this.handleGetTournamentUpdateScores = this.handleGetTournamentUpdateScores.bind(this);
|
||||
this.handlePostTournamentUpdateScores = this.handlePostTournamentUpdateScores.bind(this);
|
||||
this.handleWildcardGet = this.handleWildcardGet.bind(this);
|
||||
this.handleWildcardPost = this.handleWildcardPost.bind(this);
|
||||
|
||||
// Initialize properties
|
||||
this.prodwsurl = "https://jmcs-prod.just-dance.com";
|
||||
this.FAKEWDF_ROOM = "FAKEWDF"; // Constant for the room ID
|
||||
|
||||
this.fakerecap = {
|
||||
"uniquePlayerCount": 0,
|
||||
"countries": [
|
||||
"0"
|
||||
],
|
||||
"__class": "SessionRecapInfo"
|
||||
};
|
||||
|
||||
// Pre-load static JSON data if they are small and frequently accessed
|
||||
// IMPORTANT: Adjust these paths based on your actual project structure.
|
||||
// Assuming 'database' is a sibling directory to 'plugins' if this plugin is in 'plugins'
|
||||
this.assignRoomPcData = require("../../database/data/wdf/assign-room-pc.json");
|
||||
this.newsfeedData = require("../../database/data/wdf/newsfeed.json");
|
||||
this.nextHappyHoursData = require("../../database/data/wdf/next-happyhours.json");
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the plugin's routes
|
||||
* @param {Express} app - The Express application instance
|
||||
*/
|
||||
initroute(app) {
|
||||
this.logger.info(`Initializing routes...`);
|
||||
|
||||
// Register all the WDF routes using Express app methods directly
|
||||
app.post("/wdf/v1/assign-room", this.handleAssignRoom);
|
||||
app.get("/wdf/v1/server-time", this.handleServerTime);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/screens`, this.handleScreens);
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/newsfeed`, this.handleNewsfeed);
|
||||
app.get("/wdf/v1/online-bosses", this.handleOnlineBosses);
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/next-happyhours`, this.handleNextHappyHours);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/notification`, this.handleGetNotification);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/notification`, this.handlePostNotification);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session-recap`, this.handleGetSessionRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session-recap`, this.handlePostSessionRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/score-recap`, this.handleGetScoreRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/score-recap`, this.handlePostScoreRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/online-rank-widget`, this.handleGetOnlineRankWidget);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/online-rank-widget`, this.handlePostOnlineRankWidget);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handleGetSession);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handlePostSession);
|
||||
app.delete(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/session`, this.handleDeleteSession);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/ccu`, this.handleGetCcu);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/score-recap`, this.handleGetTournamentScoreRecap);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/score-recap`, this.handlePostTournamentScoreRecap);
|
||||
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/update-scores`, this.handleGetTournamentUpdateScores);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/themes/tournament/update-scores`, this.handlePostTournamentUpdateScores);
|
||||
|
||||
// Wildcard routes for forwarding requests to the official server
|
||||
app.get(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/*`, this.handleWildcardGet);
|
||||
app.post(`/wdf/v1/rooms/${this.FAKEWDF_ROOM}/*`, this.handleWildcardPost);
|
||||
|
||||
this.logger.info(`Routes initialized`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/assign-room
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleAssignRoom(req, res) {
|
||||
res.send(this.assignRoomPcData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/server-time
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleServerTime(req, res) {
|
||||
res.send({
|
||||
"time": Date.now() / 1000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/screens
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleScreens(req, res) {
|
||||
res.send({
|
||||
"__class": "ScreenList",
|
||||
"screens": [{
|
||||
"__class": "Screen",
|
||||
"type": "in-game",
|
||||
"startTime": Date.now() / 1000,
|
||||
"endTime": (Date.now() / 1000) + 300,
|
||||
"theme": "vote",
|
||||
"mapName": "Despacito",
|
||||
"schedule": {
|
||||
"type": "probability",
|
||||
"theme": "MapVote",
|
||||
"occurance": {
|
||||
"next": (Date.now() / 1000) + 400,
|
||||
"prev": null
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/newsfeed
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleNewsfeed(req, res) {
|
||||
res.send(this.newsfeedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/online-bosses
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleOnlineBosses(req, res) {
|
||||
res.send({ __class: "OnlineBossDb", bosses: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/next-happyhours
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleNextHappyHours(req, res) {
|
||||
res.send(this.nextHappyHoursData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/notification
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetNotification(req, res) {
|
||||
res.send({ "__class": "Notification" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/notification
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostNotification(req, res) {
|
||||
res.send({ "__class": "Notification" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/session-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetSessionRecap(req, res) {
|
||||
res.send(this.fakerecap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/session-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostSessionRecap(req, res) {
|
||||
res.send(this.fakerecap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/online-rank-widget
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetOnlineRankWidget(req, res) {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/online-rank-widget
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostOnlineRankWidget(req, res) {
|
||||
res.send({
|
||||
"currentSeasonEndTime": 1714255200,
|
||||
"seasonNumber": 1,
|
||||
"currentSeasonDancerCount": 1,
|
||||
"previousSeasonWinner": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"currentUserOnlineRankInfo": {
|
||||
"wdfPoints": 0,
|
||||
"dc": {},
|
||||
"rank": 1,
|
||||
"__class": "WDFOnlineRankInfo"
|
||||
},
|
||||
"__class": "OnlineRankWidgetInfo"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetSession(req, res) {
|
||||
res.send('OK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostSession(req, res) {
|
||||
res.send('OK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/ccu
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetCcu(req, res) {
|
||||
res.send('0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle DELETE /wdf/v1/rooms/FAKEWDF/session
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleDeleteSession(req, res) {
|
||||
res.send('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/themes/tournament/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetTournamentScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/themes/tournament/score-recap
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostTournamentScoreRecap(req, res) {
|
||||
res.send({
|
||||
"__class": "RecapInfo",
|
||||
"currentRank": 1,
|
||||
"recapEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "RecapEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET /wdf/v1/rooms/FAKEWDF/themes/tournament/update-scores
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetTournamentUpdateScores(req, res) {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /wdf/v1/rooms/FAKEWDF/themes/tournament/update-scores
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostTournamentUpdateScores(req, res) {
|
||||
res.send({
|
||||
"__class": "UpdateScoreResult",
|
||||
"currentRank": 1,
|
||||
"scoreEntries": [{
|
||||
"name": "[BOT] WDF BOT",
|
||||
"avatar": 1,
|
||||
"country": 0,
|
||||
"skin": 1,
|
||||
"platform": "ps4",
|
||||
"portraitBorder": 0,
|
||||
"jdPoints": 13333,
|
||||
"tournamentBadge": true,
|
||||
"isSubscribed": true,
|
||||
"nameSuffix": 0,
|
||||
"__class": "ScoreEntry",
|
||||
"pid": "00000000-0000-0000-0000-000000000000",
|
||||
"score": 1.000000
|
||||
}],
|
||||
"totalPlayerCount": 1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wildcard GET requests for WDF rooms, forwarding to official server.
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleWildcardGet(req, res) {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.get(this.prodwsurl + result, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
this.logger.error(`Wildcard GET error:`, error.message);
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle wildcard POST requests for WDF rooms, forwarding to official server.
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleWildcardPost(req, res) {
|
||||
try {
|
||||
const ticket = req.header("Authorization");
|
||||
const result = req.url; // This gets the full URL path including the FAKEWDF and additional path segments
|
||||
|
||||
const response = await axios.post(this.prodwsurl + result, req.body, {
|
||||
headers: {
|
||||
'X-SkuId': '',
|
||||
'Authorization': ticket,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
res.send(response.data);
|
||||
} catch (error) {
|
||||
this.logger.error(`Wildcard POST error:`, error.message);
|
||||
res.status(error.response ? error.response.status : 500).send(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export an instance of the plugin
|
||||
module.exports = new WDFPlugin();
|
||||
8
plugins/FakeWDF/manifest.json
Normal file
8
plugins/FakeWDF/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "FakeWdfPlugin",
|
||||
"version": "1.0.0",
|
||||
"description": "Create a fake response for WDF so that the mod can run on older version.",
|
||||
"main": "FakeWdfPlugin.js",
|
||||
"author": "OpenParty",
|
||||
"execution": "init"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* Example HelloWorld Plugin for OpenParty
|
||||
* Demonstrates how to create a plugin using the new class-based architecture
|
||||
*/
|
||||
const Plugin = require('../core/classes/Plugin');
|
||||
const Plugin = require('../../core/classes/Plugin'); // Adjusted path
|
||||
|
||||
class HelloWorldPlugin extends Plugin {
|
||||
/**
|
||||
8
plugins/HelloWorld/manifest.json
Normal file
8
plugins/HelloWorld/manifest.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "HelloWorld",
|
||||
"version": "1.0.0",
|
||||
"description": "A simple Hello World plugin.",
|
||||
"main": "HelloWorld.js",
|
||||
"author": "OpenParty",
|
||||
"execution": "init"
|
||||
}
|
||||
Reference in New Issue
Block a user