feat: Improve Account System, Add Personalized Carousel, Improve Plugin Format

This commit is contained in:
ibratabian17
2025-06-07 22:41:56 +07:00
parent 77aee28c95
commit 96546a4d6f
27 changed files with 6398 additions and 221 deletions

View 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();

View 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

View 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"
}
}

File diff suppressed because it is too large Load Diff

View 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);

View 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>

View 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();

View 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"
}

View File

@@ -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 {
/**

View File

@@ -0,0 +1,8 @@
{
"name": "HelloWorld",
"version": "1.0.0",
"description": "A simple Hello World plugin.",
"main": "HelloWorld.js",
"author": "OpenParty",
"execution": "init"
}