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:
@@ -8,6 +8,7 @@ const cClass = require("./classList.json");
|
||||
const settings = require('../../settings.json');
|
||||
const SongService = require('../services/SongService');
|
||||
const MostPlayedService = require('../services/MostPlayedService');
|
||||
const AccountService = require('../services/AccountService'); // Import AccountService
|
||||
|
||||
let carousel = {}; //avoid list cached
|
||||
|
||||
@@ -92,17 +93,122 @@ function addJDVersion(songMapNames, type = "partyMap") {
|
||||
addCategories(generateCategories("Unplayable Songs", SongService.filterSongsByJDVersion(songMapNames, 404)));
|
||||
}
|
||||
|
||||
exports.generateCarousel = async (search, type = "partyMap") => {
|
||||
exports.generateCarousel = async (search, type = "partyMap", profileId = null) => {
|
||||
carousel = {};
|
||||
carousel = CloneObject(cClass.rootClass);
|
||||
carousel.actionLists = cClass.actionListsClass;
|
||||
const allSongMapNames = SongService.getAllMapNames();
|
||||
|
||||
// Dynamic Carousel System
|
||||
addCategories(generateCategories(settings.server.modName, allSongMapNames, type));
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(shuffleArray(allSongMapNames), type)));
|
||||
addCategories(generateCategories(settings.server.modName, CloneObject(shuffleArray(allSongMapNames)), type)); // Shuffle main category
|
||||
|
||||
let userProfile = null;
|
||||
if (profileId) {
|
||||
userProfile = await AccountService.getUserData(profileId);
|
||||
}
|
||||
|
||||
const allSongsData = SongService.getAllSongs(); // Get all song details once
|
||||
|
||||
if (userProfile) {
|
||||
let recommendedSongs = [];
|
||||
// 1. "Recommended For You (Based on Your Plays)"
|
||||
if (userProfile.history && Object.keys(userProfile.history).length > 0) {
|
||||
recommendedSongs = Object.entries(userProfile.history)
|
||||
.sort(([, countA], [, countB]) => countB - countA) // Sort by play count desc
|
||||
.map(([mapName]) => mapName)
|
||||
.slice(0, 24);
|
||||
} else if (userProfile.scores && Object.keys(userProfile.scores).length > 0) {
|
||||
// Fallback to scores if history is not available or empty
|
||||
recommendedSongs = Object.entries(userProfile.scores)
|
||||
.filter(([, scoreData]) => scoreData && typeof scoreData.timesPlayed === 'number')
|
||||
.sort(([, scoreA], [, scoreB]) => scoreB.timesPlayed - scoreA.timesPlayed)
|
||||
.map(([mapName]) => mapName)
|
||||
.slice(0, 24);
|
||||
}
|
||||
|
||||
if (recommendedSongs.length > 0) {
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(recommendedSongs), type));
|
||||
} else {
|
||||
// Fallback if no play history or scores with timesPlayed
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(shuffleArray(allSongMapNames)), type));
|
||||
}
|
||||
|
||||
// 2. "More from Artists You Enjoy"
|
||||
const artistCounts = {};
|
||||
const playedAndFavoritedSongs = new Set([
|
||||
...(userProfile.history ? Object.keys(userProfile.history) : []),
|
||||
...(userProfile.favorites ? Object.keys(userProfile.favorites) : [])
|
||||
]);
|
||||
|
||||
playedAndFavoritedSongs.forEach(mapName => {
|
||||
const song = allSongsData[mapName];
|
||||
if (song && song.artist) {
|
||||
artistCounts[song.artist] = (artistCounts[song.artist] || 0) +
|
||||
(userProfile.history?.[mapName] || 1); // Weight by play count or 1 for favorite
|
||||
}
|
||||
});
|
||||
|
||||
const topArtists = Object.entries(artistCounts)
|
||||
.sort(([, countA], [, countB]) => countB - countA)
|
||||
.slice(0, 3) // Get top 3 artists
|
||||
.map(([artist]) => artist);
|
||||
|
||||
topArtists.forEach(artistName => {
|
||||
const artistSongs = allSongMapNames.filter(mapName => {
|
||||
const song = allSongsData[mapName];
|
||||
return song && song.artist === artistName && !playedAndFavoritedSongs.has(mapName); // Exclude already prominent songs
|
||||
});
|
||||
if (artistSongs.length > 0) {
|
||||
addCategories(generateCategories(`[icon:ARTIST] More from ${artistName}`, CloneObject(shuffleArray(artistSongs)).slice(0,12), type));
|
||||
}
|
||||
});
|
||||
|
||||
// 3. "Because You Liked..."
|
||||
const favoriteMaps = Object.keys(userProfile.favorites || {});
|
||||
if (favoriteMaps.length > 0) {
|
||||
const shuffledFavorites = shuffleArray(favoriteMaps);
|
||||
const numBecauseYouLiked = Math.min(shuffledFavorites.length, 2); // Max 2 "Because you liked" categories
|
||||
|
||||
for (let i = 0; i < numBecauseYouLiked; i++) {
|
||||
const favMapName = shuffledFavorites[i];
|
||||
const favSong = allSongsData[favMapName];
|
||||
if (!favSong) continue;
|
||||
|
||||
let relatedSongs = [];
|
||||
// Related by artist
|
||||
allSongMapNames.forEach(mapName => {
|
||||
const song = allSongsData[mapName];
|
||||
if (song && song.artist === favSong.artist && mapName !== favMapName && !favoriteMaps.includes(mapName)) {
|
||||
relatedSongs.push(mapName);
|
||||
}
|
||||
});
|
||||
// Related by original JD version
|
||||
allSongMapNames.forEach(mapName => {
|
||||
const song = allSongsData[mapName];
|
||||
if (song && song.originalJDVersion === favSong.originalJDVersion && mapName !== favMapName && !favoriteMaps.includes(mapName) && !relatedSongs.includes(mapName)) {
|
||||
relatedSongs.push(mapName);
|
||||
}
|
||||
});
|
||||
|
||||
if (relatedSongs.length > 0) {
|
||||
addCategories(generateCategories(`[icon:HEART] Because You Liked ${favSong.title}`, CloneObject(shuffleArray(relatedSongs)).slice(0, 10), type));
|
||||
}
|
||||
}
|
||||
// Original "Your Favorites" category
|
||||
addCategories(generateCategories("[icon:FAVORITE] Your Favorites", CloneObject(favoriteMaps), type));
|
||||
}
|
||||
|
||||
// Your Recently Played
|
||||
const recentlyPlayedMaps = (userProfile.songsPlayed || []).slice(-24).reverse(); // Get last 24 played, most recent first
|
||||
if (recentlyPlayedMaps.length > 0) {
|
||||
addCategories(generateCategories("[icon:HISTORY] Your Recently Played", CloneObject(recentlyPlayedMaps), type));
|
||||
}
|
||||
} else {
|
||||
// Fallback for non-logged in users or no profileId
|
||||
addCategories(generateCategories("Recommended For You", CloneObject(shuffleArray(allSongMapNames)), type));
|
||||
}
|
||||
|
||||
addCategories(generateCategories("[icon:PLAYLIST]Recently Added!", CloneObject(SongService.filterSongsByTags(allSongMapNames, 'NEW')), type));
|
||||
const path = require('path');
|
||||
await generateWeeklyRecommendedSong(loadJsonFile('carousel/playlist.json', '../database/data/carousel/playlist.json'), type);
|
||||
processPlaylists(loadJsonFile('carousel/playlist.json', '../database/data/carousel/playlist.json'), type);
|
||||
addJDVersion(allSongMapNames, type);
|
||||
|
||||
@@ -7,7 +7,7 @@ const { resolvePath } = require('../helper');
|
||||
const PluginManager = require('./PluginManager');
|
||||
const Router = require('./Router');
|
||||
const ErrorHandler = require('./ErrorHandler');
|
||||
const bodyParser = require('body-parser');
|
||||
const express = require('express'); // bodyParser is part of express now
|
||||
const requestIp = require('../lib/ipResolver.js');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
@@ -20,6 +20,7 @@ class Core {
|
||||
this.settings = settings;
|
||||
this.pluginManager = new PluginManager();
|
||||
this.router = new Router();
|
||||
this.appInstance = null; // To store app instance for plugins if needed
|
||||
this.logger = new Logger('CORE');
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ class Core {
|
||||
*/
|
||||
async init(app, express, server) {
|
||||
this.logger.info('Initializing core...');
|
||||
this.appInstance = app; // Store app instance
|
||||
|
||||
// Initialize the database
|
||||
const { initializeDatabase } = require('../database/sqlite');
|
||||
@@ -42,8 +44,11 @@ class Core {
|
||||
process.exit(1); // Exit if database cannot be initialized
|
||||
}
|
||||
|
||||
// Set pluginManager on the app instance so plugins can access it
|
||||
app.set('pluginManager', this.pluginManager);
|
||||
|
||||
// Configure middleware
|
||||
this.configureMiddleware(app, express);
|
||||
this.configureMiddleware(app); // express module not needed here anymore
|
||||
|
||||
// Load plugins
|
||||
this.pluginManager.loadPlugins(this.settings.modules);
|
||||
@@ -68,9 +73,10 @@ class Core {
|
||||
* @param {Express} app - The Express application instance
|
||||
* @param {Express} express - The Express module
|
||||
*/
|
||||
configureMiddleware(app, express) {
|
||||
configureMiddleware(app) {
|
||||
app.use(express.json());
|
||||
app.use(bodyParser.raw());
|
||||
app.use(express.urlencoded({ extended: true })); // Added for form data
|
||||
// app.use(express.raw()); // If you need raw body parsing, uncomment this and ensure AdminPanelPlugin doesn't re-add it.
|
||||
app.use(requestIp.mw());
|
||||
|
||||
// Use centralized error handler
|
||||
|
||||
@@ -14,7 +14,8 @@ class Plugin {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.enabled = true;
|
||||
this.logger = new Logger(name); // Use plugin name as module name for logger
|
||||
this.logger = new Logger(name); // Use name as module name for logger
|
||||
this.manifest = null; // To store manifest data
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Plugin = require('./Plugin'); // This is the Plugin class PluginManager uses for comparison
|
||||
const { resolvePath } = require('../helper');
|
||||
const Plugin = require('./Plugin');
|
||||
const Logger = require('../utils/logger');
|
||||
|
||||
class PluginManager {
|
||||
@@ -19,37 +18,86 @@ class PluginManager {
|
||||
|
||||
/**
|
||||
* Load plugins from settings
|
||||
* @param {Object} modules - The modules configuration from settings.json
|
||||
* @returns {Map} The loaded plugins
|
||||
*/
|
||||
loadPlugins(modules) {
|
||||
this.logger.info('Loading plugins...');
|
||||
loadPlugins() {
|
||||
this.logger.info('Loading plugins from plugins directory...');
|
||||
this.plugins.clear(); // Clear existing plugins before reloading
|
||||
|
||||
// Log the Plugin class that PluginManager is using for comparison
|
||||
this.logger.info('Plugin class used for comparison:', Plugin.name);
|
||||
const pluginsDir = path.resolve(__dirname, '../../plugins');
|
||||
|
||||
if (!fs.existsSync(pluginsDir)) {
|
||||
this.logger.warn(`Plugins directory not found: ${pluginsDir}`);
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
const pluginFolders = fs.readdirSync(pluginsDir, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
|
||||
pluginFolders.forEach(folderName => {
|
||||
const pluginFolderPath = path.join(pluginsDir, folderName);
|
||||
const manifestPath = path.join(pluginFolderPath, 'manifest.json');
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
this.logger.warn(`Manifest.json not found in plugin folder: ${folderName}. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
modules.forEach((item) => {
|
||||
try {
|
||||
const plugin = require(resolvePath(item.path));
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
||||
|
||||
// Log the Plugin class that the loaded plugin is extending
|
||||
this.logger.info(`Loaded plugin '${item.path}' extends:`, Object.getPrototypeOf(plugin.constructor).name);
|
||||
if (!manifest.name || !manifest.main || !manifest.execution) {
|
||||
this.logger.error(`Invalid manifest.json in ${folderName}: Missing name, main, or execution. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that the plugin extends the Plugin class
|
||||
if (plugin instanceof Plugin) {
|
||||
this.plugins.set(plugin.name, plugin);
|
||||
this.logger.info(`Loaded plugin: ${plugin.name}`);
|
||||
const mainPluginFile = path.join(pluginFolderPath, manifest.main);
|
||||
if (!fs.existsSync(mainPluginFile)) {
|
||||
this.logger.error(`Main plugin file '${manifest.main}' not found in ${folderName} at ${mainPluginFile}. Skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pluginInstance = require(mainPluginFile);
|
||||
|
||||
if (pluginInstance instanceof Plugin) {
|
||||
const originalPluginName = pluginInstance.name;
|
||||
pluginInstance.manifest = manifest;
|
||||
pluginInstance.name = manifest.name; // Override name from manifest
|
||||
pluginInstance.description = manifest.description || pluginInstance.description; // Override description
|
||||
|
||||
// If the name was overridden by the manifest, update the logger instance to use the new name
|
||||
if (pluginInstance.logger.moduleName !== manifest.name) {
|
||||
this.logger.info(`Plugin class-defined name ('${originalPluginName}') differs from manifest ('${manifest.name}') for plugin in folder '${folderName}'. Updating logger to use manifest name '${manifest.name}'.`);
|
||||
pluginInstance.logger = new Logger(manifest.name); // Re-initialize logger with manifest name
|
||||
}
|
||||
|
||||
this.plugins.set(manifest.name, pluginInstance);
|
||||
this.logger.info(`Loaded plugin: ${manifest.name} (v${manifest.version || 'N/A'}) from ${folderName}`);
|
||||
} else {
|
||||
this.logger.error(`Error: ${item.path} is not a valid plugin. It does not extend the expected 'Plugin' class.`);
|
||||
// Provide more detail if the instanceof check fails
|
||||
this.logger.error(`Expected Plugin constructor:`, Plugin);
|
||||
this.logger.error(`Actual plugin's prototype chain constructor:`, Object.getPrototypeOf(plugin.constructor));
|
||||
this.logger.error(`Error: ${mainPluginFile} from ${folderName} is not a valid plugin. It does not extend the 'Plugin' class.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error loading plugin ${item.path}: ${error.message}`);
|
||||
this.logger.error(`Error loading plugin from ${folderName}: ${error.message}\n${error.stack}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Process overrides
|
||||
const pluginsToOverride = new Set();
|
||||
this.plugins.forEach(pInstance => {
|
||||
if (pInstance.manifest && Array.isArray(pInstance.manifest.override)) {
|
||||
pInstance.manifest.override.forEach(pluginNameToOverride => {
|
||||
pluginsToOverride.add(pluginNameToOverride);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
pluginsToOverride.forEach(pluginNameToOverride => {
|
||||
if (this.plugins.has(pluginNameToOverride) && this.plugins.get(pluginNameToOverride).isEnabled()) {
|
||||
this.logger.info(`Plugin '${pluginNameToOverride}' is being overridden and will be disabled by another plugin.`);
|
||||
this.plugins.get(pluginNameToOverride).disable();
|
||||
}
|
||||
});
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
@@ -61,23 +109,21 @@ class PluginManager {
|
||||
initializePlugins(app, executionType) {
|
||||
this.logger.info(`Initializing ${executionType} plugins...`);
|
||||
|
||||
this.plugins.forEach((plugin) => {
|
||||
// Assuming isEnabled() exists on the Plugin base class or is handled otherwise
|
||||
if (plugin.isEnabled && plugin.isEnabled()) {
|
||||
this.plugins.forEach((pluginInstance) => {
|
||||
if (pluginInstance.manifest && pluginInstance.isEnabled && pluginInstance.isEnabled()) {
|
||||
try {
|
||||
// Get the plugin's configuration from settings.json
|
||||
const pluginConfig = this.getPluginConfig(plugin.name);
|
||||
if (pluginConfig && pluginConfig.execution === executionType) {
|
||||
this.logger.info(`Calling initroute for plugin: ${plugin.name} (Execution Type: ${executionType})`);
|
||||
plugin.initroute(app);
|
||||
if (pluginInstance.manifest.execution === executionType) {
|
||||
this.logger.info(`Calling initroute for plugin: ${pluginInstance.name} (Execution Type: ${executionType})`);
|
||||
pluginInstance.initroute(app);
|
||||
} else {
|
||||
this.logger.info(`Skipping plugin ${plugin.name}: Execution type mismatch or no config.`);
|
||||
// This log can be verbose, uncomment if needed for debugging
|
||||
// this.logger.info(`Skipping plugin ${pluginInstance.name}: Execution type mismatch (Plugin: ${pluginInstance.manifest.execution}, Required: ${executionType}).`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error initializing plugin ${plugin.name}: ${error.message}`);
|
||||
this.logger.error(`Error initializing plugin ${pluginInstance.name}: ${error.message}\n${error.stack}`);
|
||||
}
|
||||
} else {
|
||||
this.logger.info(`Skipping disabled plugin: ${plugin.name}`);
|
||||
} else if (pluginInstance.manifest && (!pluginInstance.isEnabled || !pluginInstance.isEnabled())) {
|
||||
this.logger.info(`Skipping disabled plugin: ${pluginInstance.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -98,24 +144,6 @@ class PluginManager {
|
||||
getPlugins() {
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration for a plugin from settings.json
|
||||
* @param {string} name - The name of the plugin
|
||||
* @returns {Object|null} The plugin configuration or null if not found
|
||||
*/
|
||||
getPluginConfig(name) {
|
||||
// IMPORTANT: Adjust this path if your settings.json is not located relative to PluginManager.js
|
||||
// For example, if PluginManager is in 'core/classes' and settings.json is in the root,
|
||||
// '../../settings.json' is likely correct.
|
||||
try {
|
||||
const settings = require('../../settings.json');
|
||||
return settings.modules.find(module => module.name === name) || null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error loading settings.json: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PluginManager;
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
*/
|
||||
const axios = require('axios');
|
||||
const RouteHandler = require('./RouteHandler'); // Assuming RouteHandler is in the same directory
|
||||
const { updateMostPlayed } = require('../../carousel/carousel'); // Adjust path as needed
|
||||
const MostPlayedService = require('../../services/MostPlayedService');
|
||||
const AccountService = require('../../services/AccountService'); // Import the AccountService
|
||||
const { getDb } = require('../../database/sqlite');
|
||||
const Logger = require('../../utils/logger');
|
||||
const { v4: uuidv4 } = require('uuid'); // Import uuid for generating new profile IDs
|
||||
|
||||
class AccountRouteHandler extends RouteHandler {
|
||||
/**
|
||||
@@ -122,6 +123,66 @@ class AccountRouteHandler extends RouteHandler {
|
||||
return AccountService.getUserData(profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Read the last reset week from the database.
|
||||
* @returns {Promise<number>} The last reset week number.
|
||||
* @private
|
||||
*/
|
||||
async readLastResetWeek() {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get('SELECT value FROM config WHERE key = ?', ['last_reset_week'], (err, row) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error reading last_reset_week from DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row ? parseInt(row.value, 10) : 0); // Default to 0 if not found
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Write the current week to the database.
|
||||
* @param {number} weekNumber - The current week number.
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async writeLastResetWeek(weekNumber) {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)', ['last_reset_week', weekNumber.toString()], (err) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error writing last_reset_week to DB: ${err.message}`);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info(`Updated last_reset_week in DB to week ${weekNumber}`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Clear all entries from the main leaderboard table.
|
||||
* @returns {Promise<void>} A promise that resolves when the table is cleared.
|
||||
* @private
|
||||
*/
|
||||
async clearLeaderboard() {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM leaderboard', [], (err) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error clearing leaderboard table:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info(`Cleared leaderboard table.`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Read leaderboard data from SQLite.
|
||||
* @param {boolean} isDotw - True if reading Dancer of the Week leaderboard.
|
||||
@@ -147,11 +208,14 @@ class AccountRouteHandler extends RouteHandler {
|
||||
data[row.mapName].push(row);
|
||||
});
|
||||
} else {
|
||||
// For DOTW, assume a single entry per week or handle as needed
|
||||
// For now, just return the rows as an array, or the first row if only one is expected
|
||||
// For DOTW, return the week number of the first entry if available
|
||||
// and all entries.
|
||||
if (rows.length > 0) {
|
||||
data.week = this.getWeekNumber(); // Assuming 'week' property is used to check current week
|
||||
data.week = rows[0].weekNumber; // Assuming weekNumber is stored in the row
|
||||
data.entries = rows;
|
||||
} else {
|
||||
data.week = null;
|
||||
data.entries = [];
|
||||
}
|
||||
}
|
||||
resolve(data);
|
||||
@@ -160,6 +224,26 @@ class AccountRouteHandler extends RouteHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Clear all entries from the dotw leaderboard table.
|
||||
* @returns {Promise<void>} A promise that resolves when the table is cleared.
|
||||
* @private
|
||||
*/
|
||||
async clearDotwLeaderboard() {
|
||||
const db = getDb();
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run('DELETE FROM dotw', [], (err) => {
|
||||
if (err) {
|
||||
this.logger.error(`Error clearing dotw table:`, err.message);
|
||||
reject(err);
|
||||
} else {
|
||||
this.logger.info(`Cleared dotw table.`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Generate a leaderboard object from user data.
|
||||
* This method is now primarily for transforming user data into a format suitable for leaderboard entries,
|
||||
@@ -218,7 +302,11 @@ class AccountRouteHandler extends RouteHandler {
|
||||
if (isDotw) {
|
||||
stmt = db.prepare(`INSERT OR REPLACE INTO ${tableName} (mapName, profileId, username, score, timestamp, gameVersion, rank, name, avatar, country, platformId, alias, aliasGender, jdPoints, portraitBorder, weekNumber) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
} else {
|
||||
stmt = db.prepare(`INSERT OR REPLACE INTO ${tableName} (mapName, profileId, username, score, timestamp) VALUES (?, ?, ?, ?, ?)`);
|
||||
stmt = db.prepare(`INSERT OR REPLACE INTO ${tableName} (
|
||||
mapName, profileId, username, score, timestamp, name,
|
||||
gameVersion, rank, avatar, country, platformId,
|
||||
alias, aliasGender, jdPoints, portraitBorder
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
||||
}
|
||||
|
||||
const promises = leaderboardEntries.map(entry => {
|
||||
@@ -253,6 +341,16 @@ class AccountRouteHandler extends RouteHandler {
|
||||
entry.username,
|
||||
entry.score,
|
||||
entry.timestamp,
|
||||
entry.name,
|
||||
entry.gameVersion,
|
||||
entry.rank,
|
||||
entry.avatar,
|
||||
entry.country,
|
||||
entry.platformId,
|
||||
entry.alias,
|
||||
entry.aliasGender,
|
||||
entry.jdPoints,
|
||||
entry.portraitBorder,
|
||||
(err) => {
|
||||
if (err) rejectRun(err);
|
||||
else resolveRun();
|
||||
@@ -297,44 +395,42 @@ class AccountRouteHandler extends RouteHandler {
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handlePostProfiles(req, res) {
|
||||
const profileId = req.body?.profileId || req.body?.userId;
|
||||
const authHeader = req.header('Authorization');
|
||||
const ticket = authHeader ? authHeader : null; // Keep the full header as ticket
|
||||
|
||||
if (!profileId) {
|
||||
return res.status(400).send({ message: "Missing profileId or userId" });
|
||||
if (!ticket) {
|
||||
this.logger.warn(`POST /profile/v2/profiles: Missing Authorization header (ticket).`);
|
||||
return res.status(400).send({ message: "Missing Authorization header (ticket)." });
|
||||
}
|
||||
|
||||
const userData = await AccountService.getUserData(profileId); // Await getUserData
|
||||
const profileId = await AccountService.findUserFromTicket(ticket);
|
||||
|
||||
if (!profileId) {
|
||||
this.logger.warn(`POST /profile/v2/profiles: Profile not found for provided ticket.`);
|
||||
return res.status(400).send({ message: "Profile not found for provided ticket." });
|
||||
}
|
||||
|
||||
let userData = await AccountService.getUserData(profileId);
|
||||
|
||||
if (userData) {
|
||||
this.logger.info(`Updating existing profile ${profileId}`);
|
||||
|
||||
// Update only the fields present in the request body, preserving other fields
|
||||
const updatedProfile = await AccountService.updateUser(profileId, req.body); // Await updateUser
|
||||
// Ensure the ticket is updated if present in the header
|
||||
const updateData = { ...req.body };
|
||||
updateData.ticket = ticket; // Always update ticket from header
|
||||
const updatedProfile = await AccountService.updateUser(profileId, updateData);
|
||||
|
||||
return res.send({
|
||||
__class: "UserProfile",
|
||||
...updatedProfile.toJSON()
|
||||
...updatedProfile.toPublicJSON()
|
||||
});
|
||||
} else {
|
||||
this.logger.info(`Creating new profile ${profileId}`);
|
||||
|
||||
// Create a new profile with default values and request body values
|
||||
const newProfile = await AccountService.updateUser(profileId, { // Await updateUser
|
||||
...req.body,
|
||||
name: req.body.name || "Player",
|
||||
alias: req.body.alias || "default",
|
||||
aliasGender: req.body.aliasGender || 2,
|
||||
scores: req.body.scores || {},
|
||||
songsPlayed: req.body.songsPlayed || [],
|
||||
avatar: req.body.avatar || "UI/menu_avatar/base/light.png",
|
||||
country: req.body.country || "US",
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
return res.send({
|
||||
__class: "UserProfile",
|
||||
...newProfile.toJSON()
|
||||
});
|
||||
// This case should ideally not be reached if profileId is always found via ticket
|
||||
// However, if it is, it means a ticket was provided but no profile exists for it.
|
||||
// As per previous instruction, we should not create a new profile here.
|
||||
this.logger.error(`POST /profile/v2/profiles: Unexpected state - profileId found via ticket but no existing user data.`);
|
||||
return res.status(400).send({ message: "Profile not found for provided ticket." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,51 +440,51 @@ class AccountRouteHandler extends RouteHandler {
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
async handleGetProfiles(req, res) {
|
||||
// Get the profileId from query parameters or authorization header
|
||||
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization')); // Await findUserFromTicket
|
||||
const profileIdsParam = req.query.profileIds;
|
||||
|
||||
if (profileIdsParam) {
|
||||
try {
|
||||
const requestedProfileIds = profileIdsParam.split(',');
|
||||
const profiles = [];
|
||||
|
||||
for (const reqProfileId of requestedProfileIds) {
|
||||
const profile = await AccountService.getUserData(reqProfileId);
|
||||
if (profile) {
|
||||
profiles.push({
|
||||
__class: "UserProfile",
|
||||
...profile.toPublicJSON()
|
||||
});
|
||||
} else {
|
||||
profiles.push({
|
||||
profileId: reqProfileId,
|
||||
isExisting: false
|
||||
});
|
||||
}
|
||||
}
|
||||
return res.send(profiles);
|
||||
} catch (error) {
|
||||
this.logger.error('Error processing profileIds:', error);
|
||||
return res.status(400).send({ message: "Invalid profileIds format" });
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for single profile request or if profileIds is not present
|
||||
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization'));
|
||||
|
||||
if (!profileId) {
|
||||
return res.status(400).send({ message: "Missing profileId" });
|
||||
}
|
||||
|
||||
const userProfile = await AccountService.getUserData(profileId); // Await getUserData
|
||||
const userProfile = await AccountService.getUserData(profileId);
|
||||
|
||||
if (!userProfile) {
|
||||
this.logger.info(`Profile ${profileId} not found`);
|
||||
return res.status(404).send({ message: "Profile not found" });
|
||||
}
|
||||
|
||||
// If query contains specific profile requests by IDs
|
||||
if (req.query.requestedProfiles) {
|
||||
try {
|
||||
const requestedProfiles = JSON.parse(req.query.requestedProfiles);
|
||||
const profiles = {};
|
||||
|
||||
// Get each requested profile
|
||||
for (const reqProfileId of requestedProfiles) {
|
||||
const profile = await AccountService.getUserData(reqProfileId); // Await getUserData
|
||||
if (profile) {
|
||||
profiles[reqProfileId] = {
|
||||
__class: "UserProfile",
|
||||
...profile.toJSON()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return res.send({
|
||||
__class: "ProfilesContainer",
|
||||
profiles: profiles
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Error parsing requestedProfiles:', error);
|
||||
return res.status(400).send({ message: "Invalid requestedProfiles format" });
|
||||
}
|
||||
}
|
||||
|
||||
// Return single profile
|
||||
return res.send({
|
||||
__class: "UserProfile",
|
||||
...userProfile.toJSON()
|
||||
...userProfile.toPublicJSON()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -399,7 +495,7 @@ class AccountRouteHandler extends RouteHandler {
|
||||
*/
|
||||
async handleMapEnded(req, res) {
|
||||
const { mapName, score } = req.body;
|
||||
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization')); // Await findUserFromTicket
|
||||
const profileId = req.query.profileId || await this.findUserFromTicket(req.header('Authorization'));
|
||||
|
||||
if (!profileId) {
|
||||
return res.status(400).send({ message: "Missing profileId" });
|
||||
@@ -409,21 +505,32 @@ class AccountRouteHandler extends RouteHandler {
|
||||
return res.status(400).send({ message: "Missing mapName or score" });
|
||||
}
|
||||
|
||||
const userProfile = await AccountService.getUserData(profileId); // Await getUserData
|
||||
const userProfile = await AccountService.getUserData(profileId);
|
||||
|
||||
if (!userProfile) {
|
||||
this.logger.info(`Profile ${profileId} not found`);
|
||||
return res.status(404).send({ message: "Profile not found" });
|
||||
}
|
||||
|
||||
// Perform weekly leaderboard reset check
|
||||
const currentWeek = this.getWeekNumber();
|
||||
const lastResetWeek = await this.readLastResetWeek();
|
||||
|
||||
if (currentWeek !== lastResetWeek) {
|
||||
this.logger.info(`New week detected: ${currentWeek}. Resetting all leaderboards.`);
|
||||
await this.clearLeaderboard(); // Clear main leaderboard
|
||||
await this.clearDotwLeaderboard(); // Clear DOTW leaderboard
|
||||
await this.writeLastResetWeek(currentWeek); // Update last reset week
|
||||
}
|
||||
|
||||
// Update most played maps
|
||||
updateMostPlayed(mapName);
|
||||
await MostPlayedService.updateMostPlayed(mapName);
|
||||
|
||||
// Update user's score for this map
|
||||
const currentScore = userProfile.scores?.[mapName]?.highest || 0;
|
||||
const newHighest = Math.max(currentScore, score);
|
||||
|
||||
await AccountService.updateUserScore(profileId, mapName, { // Await updateUserScore
|
||||
await AccountService.updateUserScore(profileId, mapName, {
|
||||
highest: newHighest,
|
||||
lastPlayed: new Date().toISOString(),
|
||||
history: [
|
||||
@@ -437,24 +544,36 @@ class AccountRouteHandler extends RouteHandler {
|
||||
|
||||
// Add to songsPlayed array if not already present
|
||||
if (!userProfile.songsPlayed?.includes(mapName)) {
|
||||
await AccountService.updateUser(profileId, { // Await updateUser
|
||||
await AccountService.updateUser(profileId, {
|
||||
songsPlayed: [...(userProfile.songsPlayed || []), mapName]
|
||||
});
|
||||
}
|
||||
|
||||
// Update leaderboards
|
||||
// Update leaderboards (main and DOTW)
|
||||
const allAccounts = await AccountService.getAllAccounts();
|
||||
const leaderboardEntries = this.generateLeaderboard(allAccounts, req);
|
||||
await this.saveLeaderboard(leaderboardEntries);
|
||||
await this.saveLeaderboard(leaderboardEntries); // Save to main leaderboard
|
||||
|
||||
// Update DOTW (Dancer of the Week) leaderboard if it's a new week
|
||||
const currentWeek = this.getWeekNumber();
|
||||
const dotwData = await this.readLeaderboard(true);
|
||||
|
||||
if (!dotwData.week || dotwData.week !== currentWeek) {
|
||||
this.logger.info(`New week detected: ${currentWeek}, resetting DOTW`);
|
||||
await this.saveLeaderboard([], true);
|
||||
// Save current map's score to DOTW
|
||||
await this.saveLeaderboard([
|
||||
{
|
||||
mapName: mapName,
|
||||
profileId: profileId,
|
||||
username: userProfile.name,
|
||||
score: newHighest,
|
||||
timestamp: new Date().toISOString(),
|
||||
gameVersion: this.getGameVersion(req),
|
||||
rank: userProfile.rank,
|
||||
name: userProfile.name,
|
||||
avatar: userProfile.avatar,
|
||||
country: userProfile.country,
|
||||
platformId: userProfile.platformId,
|
||||
alias: userProfile.alias,
|
||||
aliasGender: userProfile.aliasGender,
|
||||
jdPoints: userProfile.jdPoints,
|
||||
portraitBorder: userProfile.portraitBorder
|
||||
}
|
||||
], true); // Save to DOTW leaderboard
|
||||
|
||||
return res.send({
|
||||
__class: "MapEndResult",
|
||||
@@ -504,9 +623,42 @@ class AccountRouteHandler extends RouteHandler {
|
||||
return res.status(404).send({ message: "Profile not found" });
|
||||
}
|
||||
|
||||
const clientIp = req.ip; // Get client IP
|
||||
const authHeader = req.header('Authorization');
|
||||
const ticket = authHeader ? authHeader.split(' ')[1] : null; // Extract ticket from "Ubi_v1 <ticket>"
|
||||
|
||||
// Determine platformType from X-SkuId
|
||||
const skuId = req.header('X-SkuId') || '';
|
||||
let platformType = 'pc'; // Default
|
||||
if (skuId.includes('nx')) {
|
||||
platformType = 'switch';
|
||||
} else if (skuId.includes('ps4') || skuId.includes('orbis')) {
|
||||
platformType = 'ps4';
|
||||
} else if (skuId.includes('xboxone') || skuId.includes('durango')) {
|
||||
platformType = 'xboxone';
|
||||
} else if (skuId.includes('wiiu')) {
|
||||
platformType = 'wiiu';
|
||||
}
|
||||
|
||||
const serverTime = new Date().toISOString();
|
||||
const expiration = new Date(Date.now() + 3600 * 1000).toISOString(); // Expires in 1 hour
|
||||
|
||||
return res.send({
|
||||
sessionId: profileId,
|
||||
trackingEnabled: false
|
||||
platformType: platformType,
|
||||
ticket: ticket,
|
||||
twoFactorAuthenticationTicket: null,
|
||||
profileId: userProfile.profileId,
|
||||
userId: userProfile.userId, // Assuming userId is stored in Account model
|
||||
nameOnPlatform: userProfile.name || userProfile.nickname,
|
||||
environment: "Prod", // Static for now
|
||||
expiration: expiration,
|
||||
spaceId: "cd052712-ba1d-453a-89b9-08778888d380", // Static from example
|
||||
clientIp: clientIp,
|
||||
clientIpCountry: "US", // Static for now, requires IP lookup for dynamic
|
||||
serverTime: serverTime,
|
||||
sessionId: userProfile.profileId, // Keeping profileId as sessionId as per current implementation
|
||||
sessionKey: "KJNTFMD24XOPTmpgGU3MXPuhAx3IMYSYG4YgyhPJ8rVkQDHzK1MmiOtHKrQiyL/HCOsJNCfX63oRAsyGe9CDiQ==", // Placeholder
|
||||
rememberMeTicket: null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const RouteHandler = require('./RouteHandler');
|
||||
const CarouselService = require('../../services/CarouselService');
|
||||
const coreMain = require('../../var').main; // Assuming core.main is needed for various carousel data
|
||||
const Logger = require('../../utils/logger');
|
||||
const AccountService = require('../../services/AccountService'); // Import AccountService to get profileId
|
||||
|
||||
class CarouselRouteHandler extends RouteHandler {
|
||||
constructor() {
|
||||
@@ -76,6 +77,9 @@ class CarouselRouteHandler extends RouteHandler {
|
||||
search = req.body.searchTags[0];
|
||||
}
|
||||
|
||||
// Get profileId for personalization
|
||||
const profileId = req.query.profileId || await AccountService.findUserFromTicket(req.header('Authorization'));
|
||||
|
||||
let action = null;
|
||||
let isPlaylist = false;
|
||||
|
||||
@@ -100,11 +104,13 @@ class CarouselRouteHandler extends RouteHandler {
|
||||
|
||||
if (isPlaylist) {
|
||||
// Assuming core.generatePlaylist is still needed for playlist categories
|
||||
// TODO: Potentially personalize playlists as well if needed in the future
|
||||
return res.json(require('../../lib/playlist').generatePlaylist().playlistcategory);
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
return res.send(await CarouselService.generateCarousel(search, action));
|
||||
// Pass profileId to generateCarousel for personalization
|
||||
return res.send(await CarouselService.generateCarousel(search, action, profileId));
|
||||
}
|
||||
|
||||
return res.json({});
|
||||
|
||||
@@ -75,14 +75,21 @@ class UbiservicesRouteHandler extends RouteHandler {
|
||||
// Serve application parameters for JD22
|
||||
this.registerGet(app, '/v1/applications/34ad0f04-b141-4793-bdd4-985a9175e70d/parameters', this.handleGetParametersJD22);
|
||||
|
||||
// Serve application parameters for JD21
|
||||
this.registerGet(app, '/v1/applications/c8cfd4b7-91b0-446b-8e3b-7edfa393c946/parameters', this.handleGetParametersJD21);
|
||||
|
||||
// Serve application parameters for JD18
|
||||
this.registerGet(app, '/v1/spaces/041c03fa-1735-4ea7-b5fc-c16546d092ca/parameters', this.handleGetParametersJD18);
|
||||
|
||||
// Handle user-related requests (stubbed for now)
|
||||
this.registerGet(app, '/v3/policies/:langID', this.handleGetPolicies); // New policies route
|
||||
this.registerPost(app, '/v3/users', this.handlePostUsersNew); // New POST /v3/users route
|
||||
this.registerPost(app, '/v3/users/:user', this.handlePostUsers);
|
||||
this.registerGet(app, '/v3/users/:user', this.handleGetUsers);
|
||||
|
||||
|
||||
|
||||
|
||||
console.log(`[ROUTE] ${this.name} routes initialized`);
|
||||
}
|
||||
|
||||
@@ -234,6 +241,20 @@ class UbiservicesRouteHandler extends RouteHandler {
|
||||
res.send(this.replaceDomainPlaceholder(require("../../database/config/v1/parameters.json"), this.settings.server.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve application parameters for JD18
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
/**
|
||||
* Serve application parameters for JD21
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetParametersJD21(req, res) {
|
||||
res.send(this.replaceDomainPlaceholder(require("../../database/config/v1/jd21/parameters.json"), this.settings.server.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve application parameters for JD18
|
||||
* @param {Request} req - The request object
|
||||
@@ -243,6 +264,60 @@ class UbiservicesRouteHandler extends RouteHandler {
|
||||
res.send(this.replaceDomainPlaceholder(require("../../database/config/v1/parameters2.json"), this.settings.server.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user-related requests (stubbed for now)
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
/**
|
||||
* Handle GET /v3/policies/:langID
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handleGetPolicies(req, res) {
|
||||
res.send({
|
||||
"termOfSaleContent": "",
|
||||
"policyAcceptance": "I accept Ubisoft's Terms of Use, Terms of Sale and Privacy Policy.",
|
||||
"policyAcceptanceIsRequired": true,
|
||||
"policyAcceptanceDefaultValue": false,
|
||||
"policyLocaleCode": "en-US",
|
||||
"minorAccount": {
|
||||
"ageRequired": 7,
|
||||
"isDigitalSignatureRequiredForAccountCreation": true,
|
||||
"privacyPolicyContent": "Basically We Need Ur Ticket and IP for assigning cracked user only. We dont care about your data."
|
||||
},
|
||||
"adultAccount": {
|
||||
"ageRequired": 18
|
||||
},
|
||||
"legalOptinsKey": "eyJ2dG91IjoiNC4wIiwidnBwIjoiNC4xIiwidnRvcyI6IjIuMSIsImx0b3UiOiJlbi1VUyIsImxwcCI6ImVuLVVTIiwibHRvcyI6ImVuLVVTIn0",
|
||||
"ageRequired": 13,
|
||||
"communicationOptInDefaultValue": true,
|
||||
"privacyPolicyContent": "Who Need Those",
|
||||
"termOfUseContent": "Who Need Those"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST /v3/users
|
||||
* @param {Request} req - The request object
|
||||
* @param {Response} res - The response object
|
||||
*/
|
||||
handlePostUsersNew(req, res) {
|
||||
res.send({
|
||||
country: 'US',
|
||||
ageGroup: 'Adult',
|
||||
email: 'MFADAMO_JD2016@ubisoft.com',
|
||||
legalOptinsKey: 'eyJ2dG91IjoiNC4wIiwidnBwIjoiNC4xIiwidnRvcyI6IjIuMSIsImx0b3UiOiJlbi1VUyIsImxwcCI6ImVuLVVTIiwibHRvcyI6ImVuLVVTIn0',
|
||||
password: '#JD2016ubi42',
|
||||
gender: 'NOT_DEFINED',
|
||||
preferredLanguage: 'FR',
|
||||
nameOnPlatform: 'MFADAMO_J_JD2016',
|
||||
accountType: 'Ubisoft',
|
||||
profileId: "f7d85441-265d-4c9c-b1e3-25af3182091a",
|
||||
userId: "4f7fc740-da32-4f2d-a81e-18faf7a1262d"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user-related requests (stubbed for now)
|
||||
* @param {Request} req - The request object
|
||||
|
||||
@@ -97,20 +97,45 @@ class DatabaseManager {
|
||||
// Create user_profiles table
|
||||
this._db.run(`CREATE TABLE IF NOT EXISTS user_profiles (
|
||||
profileId TEXT PRIMARY KEY,
|
||||
userId TEXT, -- Already present
|
||||
username TEXT,
|
||||
nickname TEXT,
|
||||
name TEXT,
|
||||
email TEXT,
|
||||
password TEXT, -- Consider hashing if storing sensitive passwords
|
||||
ticket TEXT,
|
||||
alias TEXT,
|
||||
aliasGender INTEGER,
|
||||
avatar TEXT,
|
||||
country TEXT,
|
||||
createdAt TEXT,
|
||||
ticket TEXT,
|
||||
platformId TEXT,
|
||||
jdPoints INTEGER,
|
||||
portraitBorder TEXT,
|
||||
-- Store scores and songsPlayed as JSON strings
|
||||
scores TEXT,
|
||||
songsPlayed TEXT,
|
||||
favorites TEXT
|
||||
rank INTEGER,
|
||||
scores TEXT, -- JSON stored as TEXT
|
||||
favorites TEXT, -- JSON stored as TEXT
|
||||
songsPlayed TEXT, -- JSON array stored as TEXT
|
||||
progression TEXT, -- JSON stored as TEXT
|
||||
history TEXT, -- JSON stored as TEXT
|
||||
skin TEXT,
|
||||
diamondPoints INTEGER,
|
||||
unlockedAvatars TEXT, -- JSON array stored as TEXT
|
||||
unlockedSkins TEXT, -- JSON array stored as TEXT
|
||||
unlockedAliases TEXT, -- JSON array stored as TEXT
|
||||
unlockedPortraitBorders TEXT, -- JSON array stored as TEXT
|
||||
wdfRank INTEGER,
|
||||
stars INTEGER,
|
||||
unlocks INTEGER,
|
||||
populations TEXT, -- JSON array stored as TEXT
|
||||
inProgressAliases TEXT, -- JSON array stored as TEXT
|
||||
language TEXT,
|
||||
firstPartyEnv TEXT,
|
||||
syncVersions TEXT, -- JSON stored as TEXT
|
||||
otherPids TEXT, -- JSON array stored as TEXT
|
||||
stats TEXT, -- JSON stored as TEXT
|
||||
mapHistory TEXT, -- JSON stored as TEXT
|
||||
createdAt TEXT,
|
||||
updatedAt TEXT
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
this.logger.error('Error creating user_profiles table:', err.message);
|
||||
|
||||
1600
core/database/data/tmp/logs.txt
Normal file
1600
core/database/data/tmp/logs.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,29 @@ class Account {
|
||||
this.rank = data.rank || 0;
|
||||
this.scores = data.scores || {}; // Map of mapName to score data
|
||||
this.favorites = data.favorites || {}; // User's favorite maps
|
||||
this.songsPlayed = data.songsPlayed || []; // Array of map names
|
||||
this.progression = data.progression || {};
|
||||
this.history = data.history || {}; // Example: {"MapName": playCount}
|
||||
|
||||
// New fields from extended JSON structures (assuming these were added from previous step)
|
||||
this.skin = data.skin || null;
|
||||
this.diamondPoints = data.diamondPoints || 0;
|
||||
this.unlockedAvatars = data.unlockedAvatars || [];
|
||||
this.unlockedSkins = data.unlockedSkins || [];
|
||||
this.unlockedAliases = data.unlockedAliases || [];
|
||||
this.unlockedPortraitBorders = data.unlockedPortraitBorders || [];
|
||||
this.wdfRank = data.wdfRank || 0;
|
||||
this.stars = data.stars || 0;
|
||||
this.unlocks = data.unlocks || 0;
|
||||
this.populations = data.populations || [];
|
||||
this.inProgressAliases = data.inProgressAliases || [];
|
||||
this.language = data.language || null;
|
||||
this.firstPartyEnv = data.firstPartyEnv || null;
|
||||
this.syncVersions = data.syncVersions || {};
|
||||
this.otherPids = data.otherPids || [];
|
||||
this.stats = data.stats || {};
|
||||
this.mapHistory = data.mapHistory || { classic: [], kids: [] };
|
||||
|
||||
this.createdAt = data.createdAt || new Date().toISOString();
|
||||
this.updatedAt = data.updatedAt || new Date().toISOString();
|
||||
}
|
||||
@@ -35,7 +58,105 @@ class Account {
|
||||
* @returns {Account} Updated account instance
|
||||
*/
|
||||
update(data) {
|
||||
Object.assign(this, data);
|
||||
// Helper: Check if a value is a plain object
|
||||
const _isObject = (item) => {
|
||||
return item && typeof item === 'object' && !Array.isArray(item);
|
||||
};
|
||||
|
||||
// Helper: Deeply merge source object's properties into target object
|
||||
const _deepMergeObjects = (target, source) => {
|
||||
const output = { ...target };
|
||||
for (const key in source) {
|
||||
if (source.hasOwnProperty(key)) {
|
||||
const sourceVal = source[key];
|
||||
const targetVal = output[key];
|
||||
if (_isObject(sourceVal)) {
|
||||
if (_isObject(targetVal)) {
|
||||
output[key] = _deepMergeObjects(targetVal, sourceVal);
|
||||
} else {
|
||||
// If target's property is not an object, or doesn't exist,
|
||||
// clone the source object property.
|
||||
output[key] = _deepMergeObjects({}, sourceVal);
|
||||
}
|
||||
} else {
|
||||
// For non-object properties (primitives, arrays), source overwrites target.
|
||||
// Specific array merging is handled in the main update loop.
|
||||
output[key] = sourceVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const simpleOverwriteKeys = [
|
||||
'profileId', 'userId', 'username', 'nickname', 'name', 'email', 'password', 'ticket',
|
||||
'avatar', 'country', 'platformId', 'alias', 'aliasGender', 'jdPoints',
|
||||
'portraitBorder', 'rank', 'skin', 'diamondPoints', 'wdfRank', 'stars',
|
||||
'unlocks', 'language', 'firstPartyEnv'
|
||||
];
|
||||
|
||||
const deepMergeObjectKeys = [
|
||||
'scores', 'progression', 'history', 'favorites', 'stats', 'syncVersions'
|
||||
];
|
||||
|
||||
const unionArrayKeys = [ // Arrays of unique primitive values
|
||||
'unlockedAvatars', 'unlockedSkins', 'unlockedAliases', 'unlockedPortraitBorders', 'otherPids', 'songsPlayed'
|
||||
];
|
||||
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const incomingValue = data[key];
|
||||
|
||||
if (simpleOverwriteKeys.includes(key)) {
|
||||
this[key] = incomingValue;
|
||||
} else if (deepMergeObjectKeys.includes(key)) {
|
||||
if (_isObject(incomingValue)) {
|
||||
this[key] = _deepMergeObjects(this[key] || {}, incomingValue);
|
||||
} else { // If incoming data for a deep-merge key is not an object, overwrite.
|
||||
this[key] = incomingValue;
|
||||
}
|
||||
} else if (unionArrayKeys.includes(key)) {
|
||||
if (Array.isArray(incomingValue)) {
|
||||
this[key] = [...new Set([...(this[key] || []), ...incomingValue])];
|
||||
} else { // If incoming data for a union-array key is not an array, overwrite.
|
||||
this[key] = incomingValue;
|
||||
}
|
||||
} else if (key === 'populations') {
|
||||
if (Array.isArray(incomingValue)) {
|
||||
const existingItems = this.populations || [];
|
||||
const mergedItems = [...existingItems];
|
||||
incomingValue.forEach(newItem => {
|
||||
const index = mergedItems.findIndex(ep => ep.subject === newItem.subject && ep.spaceId === newItem.spaceId);
|
||||
if (index !== -1) mergedItems[index] = _deepMergeObjects(mergedItems[index], newItem);
|
||||
else mergedItems.push(newItem);
|
||||
});
|
||||
this.populations = mergedItems;
|
||||
} else this.populations = incomingValue;
|
||||
} else if (key === 'inProgressAliases') {
|
||||
if (Array.isArray(incomingValue)) {
|
||||
const existingItems = this.inProgressAliases || [];
|
||||
const mergedItems = [...existingItems];
|
||||
incomingValue.forEach(newItem => {
|
||||
const index = mergedItems.findIndex(ea => ea.id === newItem.id);
|
||||
if (index !== -1) mergedItems[index] = _deepMergeObjects(mergedItems[index], newItem);
|
||||
else mergedItems.push(newItem);
|
||||
});
|
||||
this.inProgressAliases = mergedItems;
|
||||
} else this.inProgressAliases = incomingValue;
|
||||
} else if (key === 'mapHistory') {
|
||||
if (_isObject(incomingValue)) {
|
||||
const currentMapHistory = this.mapHistory || { classic: [], kids: [] };
|
||||
this.mapHistory = {
|
||||
classic: [...new Set([...(currentMapHistory.classic || []), ...(incomingValue.classic || [])])],
|
||||
kids: [...new Set([...(currentMapHistory.kids || []), ...(incomingValue.kids || [])])]
|
||||
};
|
||||
} else this.mapHistory = incomingValue;
|
||||
} else if (this.hasOwnProperty(key)) {
|
||||
// Default for other existing properties not specially handled: overwrite
|
||||
this[key] = incomingValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updatedAt = new Date().toISOString();
|
||||
return this;
|
||||
}
|
||||
@@ -110,10 +231,83 @@ class Account {
|
||||
rank: this.rank,
|
||||
scores: this.scores,
|
||||
favorites: this.favorites,
|
||||
songsPlayed: this.songsPlayed,
|
||||
progression: this.progression,
|
||||
history: this.history,
|
||||
// New fields
|
||||
skin: this.skin,
|
||||
diamondPoints: this.diamondPoints,
|
||||
unlockedAvatars: this.unlockedAvatars,
|
||||
unlockedSkins: this.unlockedSkins,
|
||||
unlockedAliases: this.unlockedAliases,
|
||||
unlockedPortraitBorders: this.unlockedPortraitBorders,
|
||||
wdfRank: this.wdfRank,
|
||||
stars: this.stars,
|
||||
unlocks: this.unlocks,
|
||||
populations: this.populations,
|
||||
inProgressAliases: this.inProgressAliases,
|
||||
language: this.language,
|
||||
firstPartyEnv: this.firstPartyEnv,
|
||||
syncVersions: this.syncVersions,
|
||||
otherPids: this.otherPids,
|
||||
stats: this.stats,
|
||||
mapHistory: this.mapHistory,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to plain object for public API responses, excluding sensitive data.
|
||||
* @returns {Object} Sanitized plain object representation
|
||||
*/
|
||||
toPublicJSON() {
|
||||
const publicData = {
|
||||
profileId: this.profileId,
|
||||
userId: this.userId,
|
||||
username: this.username,
|
||||
nickname: this.nickname,
|
||||
name: this.name,
|
||||
avatar: this.avatar,
|
||||
country: this.country,
|
||||
platformId: this.platformId,
|
||||
alias: this.alias,
|
||||
aliasGender: this.aliasGender,
|
||||
jdPoints: this.jdPoints,
|
||||
portraitBorder: this.portraitBorder,
|
||||
rank: this.rank,
|
||||
scores: this.scores,
|
||||
favorites: this.favorites,
|
||||
songsPlayed: this.songsPlayed,
|
||||
progression: this.progression,
|
||||
history: this.history,
|
||||
// New fields
|
||||
skin: this.skin,
|
||||
diamondPoints: this.diamondPoints,
|
||||
unlockedAvatars: this.unlockedAvatars,
|
||||
unlockedSkins: this.unlockedSkins,
|
||||
unlockedAliases: this.unlockedAliases,
|
||||
unlockedPortraitBorders: this.unlockedPortraitBorders,
|
||||
wdfRank: this.wdfRank,
|
||||
stars: this.stars,
|
||||
unlocks: this.unlocks,
|
||||
populations: this.populations,
|
||||
inProgressAliases: this.inProgressAliases,
|
||||
language: this.language,
|
||||
firstPartyEnv: this.firstPartyEnv,
|
||||
syncVersions: this.syncVersions,
|
||||
otherPids: this.otherPids,
|
||||
stats: this.stats,
|
||||
mapHistory: this.mapHistory,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
};
|
||||
// Explicitly remove sensitive fields if they were somehow added
|
||||
delete publicData.email;
|
||||
delete publicData.password;
|
||||
delete publicData.ticket;
|
||||
return publicData;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Account;
|
||||
@@ -30,20 +30,48 @@ class AccountRepository {
|
||||
rows.forEach(row => {
|
||||
try {
|
||||
const accountData = {
|
||||
// Existing fields
|
||||
profileId: row.profileId,
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
password: row.password, // Should be handled securely if stored
|
||||
ticket: row.ticket,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
rank: row.rank,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : {},
|
||||
progression: row.progression ? JSON.parse(row.progression) : {},
|
||||
history: row.history ? JSON.parse(row.history) : {},
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
// New fields
|
||||
skin: row.skin,
|
||||
diamondPoints: row.diamondPoints,
|
||||
unlockedAvatars: row.unlockedAvatars ? JSON.parse(row.unlockedAvatars) : [],
|
||||
unlockedSkins: row.unlockedSkins ? JSON.parse(row.unlockedSkins) : [],
|
||||
unlockedAliases: row.unlockedAliases ? JSON.parse(row.unlockedAliases) : [],
|
||||
unlockedPortraitBorders: row.unlockedPortraitBorders ? JSON.parse(row.unlockedPortraitBorders) : [],
|
||||
wdfRank: row.wdfRank,
|
||||
stars: row.stars,
|
||||
unlocks: row.unlocks,
|
||||
populations: row.populations ? JSON.parse(row.populations) : [],
|
||||
inProgressAliases: row.inProgressAliases ? JSON.parse(row.inProgressAliases) : [],
|
||||
language: row.language,
|
||||
firstPartyEnv: row.firstPartyEnv,
|
||||
syncVersions: row.syncVersions ? JSON.parse(row.syncVersions) : {},
|
||||
otherPids: row.otherPids ? JSON.parse(row.otherPids) : [],
|
||||
stats: row.stats ? JSON.parse(row.stats) : {},
|
||||
mapHistory: row.mapHistory ? JSON.parse(row.mapHistory) : { classic: [], kids: [] }
|
||||
};
|
||||
accounts[row.profileId] = new Account(accountData);
|
||||
} catch (parseError) {
|
||||
@@ -75,27 +103,73 @@ class AccountRepository {
|
||||
const scoresJson = JSON.stringify(accountData.scores || {});
|
||||
const songsPlayedJson = JSON.stringify(accountData.songsPlayed || []);
|
||||
const favoritesJson = JSON.stringify(accountData.favorites || []);
|
||||
const progressionJson = JSON.stringify(accountData.progression || {});
|
||||
const historyJson = JSON.stringify(accountData.history || {});
|
||||
// Stringify new JSON fields
|
||||
const unlockedAvatarsJson = JSON.stringify(accountData.unlockedAvatars || []);
|
||||
const unlockedSkinsJson = JSON.stringify(accountData.unlockedSkins || []);
|
||||
const unlockedAliasesJson = JSON.stringify(accountData.unlockedAliases || []);
|
||||
const unlockedPortraitBordersJson = JSON.stringify(accountData.unlockedPortraitBorders || []);
|
||||
const populationsJson = JSON.stringify(accountData.populations || []);
|
||||
const inProgressAliasesJson = JSON.stringify(accountData.inProgressAliases || []);
|
||||
const syncVersionsJson = JSON.stringify(accountData.syncVersions || {});
|
||||
const otherPidsJson = JSON.stringify(accountData.otherPids || []);
|
||||
const statsJson = JSON.stringify(accountData.stats || {});
|
||||
const mapHistoryJson = JSON.stringify(accountData.mapHistory || { classic: [], kids: [] });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(`INSERT OR REPLACE INTO user_profiles (
|
||||
profileId, name, alias, aliasGender, avatar, country, createdAt, ticket,
|
||||
platformId, jdPoints, portraitBorder, scores, songsPlayed, favorites
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
profileId, userId, username, nickname, name, email, password, ticket,
|
||||
alias, aliasGender, avatar, country, platformId, jdPoints, portraitBorder, rank,
|
||||
scores, songsPlayed, favorites, progression, history,
|
||||
skin, diamondPoints, unlockedAvatars, unlockedSkins, unlockedAliases, unlockedPortraitBorders,
|
||||
wdfRank, stars, unlocks, populations, inProgressAliases, language, firstPartyEnv,
|
||||
syncVersions, otherPids, stats, mapHistory,
|
||||
createdAt, updatedAt
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
accountData.profileId,
|
||||
accountData.userId,
|
||||
accountData.username,
|
||||
accountData.nickname,
|
||||
accountData.name,
|
||||
accountData.email,
|
||||
accountData.password, // Ensure this is handled securely (e.g., hashed) if stored
|
||||
accountData.ticket,
|
||||
accountData.alias,
|
||||
accountData.aliasGender,
|
||||
accountData.avatar,
|
||||
accountData.country,
|
||||
accountData.createdAt,
|
||||
accountData.ticket,
|
||||
accountData.platformId,
|
||||
accountData.jdPoints,
|
||||
accountData.portraitBorder,
|
||||
accountData.rank,
|
||||
scoresJson,
|
||||
songsPlayedJson,
|
||||
favoritesJson
|
||||
favoritesJson,
|
||||
progressionJson,
|
||||
historyJson,
|
||||
// New fields
|
||||
accountData.skin,
|
||||
accountData.diamondPoints,
|
||||
unlockedAvatarsJson,
|
||||
unlockedSkinsJson,
|
||||
unlockedAliasesJson,
|
||||
unlockedPortraitBordersJson,
|
||||
accountData.wdfRank,
|
||||
accountData.stars,
|
||||
accountData.unlocks,
|
||||
populationsJson,
|
||||
inProgressAliasesJson,
|
||||
accountData.language,
|
||||
accountData.firstPartyEnv,
|
||||
syncVersionsJson,
|
||||
otherPidsJson,
|
||||
statsJson,
|
||||
mapHistoryJson,
|
||||
// Timestamps
|
||||
accountData.createdAt,
|
||||
accountData.updatedAt
|
||||
],
|
||||
(err) => {
|
||||
if (err) {
|
||||
@@ -124,20 +198,48 @@ class AccountRepository {
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
// Existing fields
|
||||
profileId: row.profileId,
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
password: row.password,
|
||||
ticket: row.ticket,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
rank: row.rank,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : {},
|
||||
progression: row.progression ? JSON.parse(row.progression) : {},
|
||||
history: row.history ? JSON.parse(row.history) : {},
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
// New fields
|
||||
skin: row.skin,
|
||||
diamondPoints: row.diamondPoints,
|
||||
unlockedAvatars: row.unlockedAvatars ? JSON.parse(row.unlockedAvatars) : [],
|
||||
unlockedSkins: row.unlockedSkins ? JSON.parse(row.unlockedSkins) : [],
|
||||
unlockedAliases: row.unlockedAliases ? JSON.parse(row.unlockedAliases) : [],
|
||||
unlockedPortraitBorders: row.unlockedPortraitBorders ? JSON.parse(row.unlockedPortraitBorders) : [],
|
||||
wdfRank: row.wdfRank,
|
||||
stars: row.stars,
|
||||
unlocks: row.unlocks,
|
||||
populations: row.populations ? JSON.parse(row.populations) : [],
|
||||
inProgressAliases: row.inProgressAliases ? JSON.parse(row.inProgressAliases) : [],
|
||||
language: row.language,
|
||||
firstPartyEnv: row.firstPartyEnv,
|
||||
syncVersions: row.syncVersions ? JSON.parse(row.syncVersions) : {},
|
||||
otherPids: row.otherPids ? JSON.parse(row.otherPids) : [],
|
||||
stats: row.stats ? JSON.parse(row.stats) : {},
|
||||
mapHistory: row.mapHistory ? JSON.parse(row.mapHistory) : { classic: [], kids: [] }
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
@@ -166,20 +268,48 @@ class AccountRepository {
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
// Existing fields
|
||||
profileId: row.profileId,
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
password: row.password,
|
||||
ticket: row.ticket,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
rank: row.rank,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : {},
|
||||
progression: row.progression ? JSON.parse(row.progression) : {},
|
||||
history: row.history ? JSON.parse(row.history) : {},
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
// New fields
|
||||
skin: row.skin,
|
||||
diamondPoints: row.diamondPoints,
|
||||
unlockedAvatars: row.unlockedAvatars ? JSON.parse(row.unlockedAvatars) : [],
|
||||
unlockedSkins: row.unlockedSkins ? JSON.parse(row.unlockedSkins) : [],
|
||||
unlockedAliases: row.unlockedAliases ? JSON.parse(row.unlockedAliases) : [],
|
||||
unlockedPortraitBorders: row.unlockedPortraitBorders ? JSON.parse(row.unlockedPortraitBorders) : [],
|
||||
wdfRank: row.wdfRank,
|
||||
stars: row.stars,
|
||||
unlocks: row.unlocks,
|
||||
populations: row.populations ? JSON.parse(row.populations) : [],
|
||||
inProgressAliases: row.inProgressAliases ? JSON.parse(row.inProgressAliases) : [],
|
||||
language: row.language,
|
||||
firstPartyEnv: row.firstPartyEnv,
|
||||
syncVersions: row.syncVersions ? JSON.parse(row.syncVersions) : {},
|
||||
otherPids: row.otherPids ? JSON.parse(row.otherPids) : [],
|
||||
stats: row.stats ? JSON.parse(row.stats) : {},
|
||||
mapHistory: row.mapHistory ? JSON.parse(row.mapHistory) : { classic: [], kids: [] }
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
@@ -208,20 +338,48 @@ class AccountRepository {
|
||||
} else if (row) {
|
||||
try {
|
||||
const accountData = {
|
||||
// Existing fields
|
||||
profileId: row.profileId,
|
||||
userId: row.userId,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
password: row.password,
|
||||
ticket: row.ticket,
|
||||
alias: row.alias,
|
||||
aliasGender: row.aliasGender,
|
||||
avatar: row.avatar,
|
||||
country: row.country,
|
||||
createdAt: row.createdAt,
|
||||
ticket: row.ticket,
|
||||
platformId: row.platformId,
|
||||
jdPoints: row.jdPoints,
|
||||
portraitBorder: row.portraitBorder,
|
||||
rank: row.rank,
|
||||
scores: row.scores ? JSON.parse(row.scores) : {},
|
||||
songsPlayed: row.songsPlayed ? JSON.parse(row.songsPlayed) : [],
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : []
|
||||
favorites: row.favorites ? JSON.parse(row.favorites) : {},
|
||||
progression: row.progression ? JSON.parse(row.progression) : {},
|
||||
history: row.history ? JSON.parse(row.history) : {},
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
// New fields
|
||||
skin: row.skin,
|
||||
diamondPoints: row.diamondPoints,
|
||||
unlockedAvatars: row.unlockedAvatars ? JSON.parse(row.unlockedAvatars) : [],
|
||||
unlockedSkins: row.unlockedSkins ? JSON.parse(row.unlockedSkins) : [],
|
||||
unlockedAliases: row.unlockedAliases ? JSON.parse(row.unlockedAliases) : [],
|
||||
unlockedPortraitBorders: row.unlockedPortraitBorders ? JSON.parse(row.unlockedPortraitBorders) : [],
|
||||
wdfRank: row.wdfRank,
|
||||
stars: row.stars,
|
||||
unlocks: row.unlocks,
|
||||
populations: row.populations ? JSON.parse(row.populations) : [],
|
||||
inProgressAliases: row.inProgressAliases ? JSON.parse(row.inProgressAliases) : [],
|
||||
language: row.language,
|
||||
firstPartyEnv: row.firstPartyEnv,
|
||||
syncVersions: row.syncVersions ? JSON.parse(row.syncVersions) : {},
|
||||
otherPids: row.otherPids ? JSON.parse(row.otherPids) : [],
|
||||
stats: row.stats ? JSON.parse(row.stats) : {},
|
||||
mapHistory: row.mapHistory ? JSON.parse(row.mapHistory) : { classic: [], kids: [] }
|
||||
};
|
||||
resolve(new Account(accountData));
|
||||
} catch (parseError) {
|
||||
|
||||
@@ -87,14 +87,37 @@ class AccountService {
|
||||
this.logger.info(`Updating user ${profileId}`);
|
||||
let account = await AccountRepository.findById(profileId);
|
||||
|
||||
const processedUserData = { ...userData };
|
||||
|
||||
// Pre-process favorites: if it's an array of mapNames, convert to model's object structure
|
||||
if (Array.isArray(processedUserData.favorites)) {
|
||||
const newFavorites = {};
|
||||
for (const mapName of processedUserData.favorites) {
|
||||
if (typeof mapName === 'string') { // Ensure items are strings
|
||||
newFavorites[mapName] = { addedAt: new Date().toISOString() };
|
||||
}
|
||||
}
|
||||
processedUserData.favorites = newFavorites;
|
||||
this.logger.info(`Processed 'favorites' array to object for profile ${profileId}`);
|
||||
}
|
||||
|
||||
// Pre-process songsPlayed: if it's a number (e.g., from older formats), ignore it
|
||||
// to prevent corrupting the 'songsPlayed' array of map names in the model.
|
||||
if (processedUserData.hasOwnProperty('songsPlayed') && typeof processedUserData.songsPlayed === 'number') {
|
||||
this.logger.warn(`Received 'songsPlayed' as a number (${processedUserData.songsPlayed}) for profile ${profileId}. This will be ignored as the model expects an array of map names for 'songsPlayed'.`);
|
||||
delete processedUserData.songsPlayed; // Do not pass it to account.update if it's a number
|
||||
}
|
||||
|
||||
// Add any other necessary pre-processing for other fields here
|
||||
|
||||
if (!account) {
|
||||
account = new Account({
|
||||
profileId,
|
||||
...userData
|
||||
...processedUserData // Use processed data for new account
|
||||
});
|
||||
this.logger.info(`Created new user ${profileId}`);
|
||||
} else {
|
||||
account.update(userData);
|
||||
account.update(processedUserData); // Pass processed data for update
|
||||
this.logger.info(`Updated existing user ${profileId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,28 +10,31 @@ class CarouselService {
|
||||
* Generate a carousel based on search criteria and type.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @param {string} type - The type of carousel (e.g., "partyMap", "sweatMap").
|
||||
* @param {string} [profileId=null] - The profile ID for personalization.
|
||||
* @returns {Object} The generated carousel object.
|
||||
*/
|
||||
generateCarousel(search, type) {
|
||||
return generateCarousel(search, type);
|
||||
generateCarousel(search, type, profileId = null) {
|
||||
return generateCarousel(search, type, profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cooperative carousel.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @param {string} [profileId=null] - The profile ID for personalization.
|
||||
* @returns {Object} The generated cooperative carousel object.
|
||||
*/
|
||||
generateCoopCarousel(search) {
|
||||
return generateCoopCarousel(search);
|
||||
generateCoopCarousel(search, profileId = null) {
|
||||
return generateCoopCarousel(search, profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a sweat carousel.
|
||||
* @param {string} search - The search string or tag.
|
||||
* @param {string} [profileId=null] - The profile ID for personalization.
|
||||
* @returns {Object} The generated sweat carousel object.
|
||||
*/
|
||||
generateSweatCarousel(search) {
|
||||
return generateSweatCarousel(search);
|
||||
generateSweatCarousel(search, profileId = null) {
|
||||
return generateSweatCarousel(search, profileId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
103
database/config/v1/jd21/parameters.json
Normal file
103
database/config/v1/jd21/parameters.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"parameters": {
|
||||
"us-staging": {
|
||||
"fields": {
|
||||
"spaceId": "59f82720-778f-427a-a0bd-e81cb112c044"
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientClub": {
|
||||
"fields": {
|
||||
"ps4ClubWebsite": "https://static8.cdn.ubi.com/u/sites/uplay/index.html",
|
||||
"xoneLaunchPath": "Ms-xbl-55C5EE27://default?url=/games/{deepLink}&context={context}&titleid={gameTitleId}&env={clubEnvName}&genomeid={applicationId}&actioncompleted={actionCompletedList}&debug={debug}&spaceId={spaceId}&applicationId={applicationId}",
|
||||
"dynamicPanelUrl": "https://{env}public-ubiservices.ubi.com/v1/profiles/{profileId}/club/dynamicPanel/MyProfile.png?spaceId={spaceId}&noRedirect=true",
|
||||
"psApiClubWebsite": "https://static8.cdn.ubi.com/u/sites/uplay/index.html",
|
||||
"ps4PsnoLaunchPath": "psno://localhost/?response_type=code&client_id=171ca84a-371e-412b-a807-6531f3f1e454&scope=psn%3As2s&redirect_uri={ps4ClubWebsiteUrlEncoded}&state={ps4ClubWebsiteArgumentsPsnoEncoded}",
|
||||
"psApiPsnoLaunchPath": "psno://localhost/?response_type=code&client_id=171ca84a-371e-412b-a807-6531f3f1e454&scope=psn%3As2s&redirect_uri={psApiClubWebsiteUrlEncoded}&state={psApiClubWebsiteArgumentsPsnoEncoded}",
|
||||
"stadiaClubWebsiteUrl": "https://static8.cdn.ubi.com/u/sites/UbisoftClub/Stadia/index.html?url=/games/{deepLink}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&env={env}&displayMode={displayMode}&locale={locale}&actioncompleted={actionCompletedList}&jotunVersion={jotunVersion}&jotunBinaryVersion={jotunBinaryVersion}&ticket={ticket}",
|
||||
"switchClubWebsiteUrl": "https://static8.cdn.ubi.com/u/sites/UbisoftClub/NintendoSwitch/index.html?url=/games/{deepLink}&ussdkversion={usSdkVersionName}&env={clubEnvName}&actioncompleted={actionCompletedList}&forceLang={forceLang}&profileId={profileId}&ticket={ticket}&context={context}&debug={debug}&spaceId={spaceId}&applicationId={applicationId}",
|
||||
"ps4ClubWebsiteArguments": "url=/games/{deepLink}&spaceId={spaceId}&applicationId={applicationId}&context={context}&env={clubEnvName}&genomeid={applicationId}&actioncompleted={actionCompletedList}&ussdkversion={usSdkVersionName}&debug={debug}{geometry}&ticket={ticket}&profileid={profileId}",
|
||||
"psApiClubWebsiteArguments": "url=/games/{deepLink}&spaceId={spaceId}&applicationId={applicationId}&context={context}&env={clubEnvName}&genomeid={applicationId}&actioncompleted={actionCompletedList}&ussdkversion={usSdkVersionName}&debug={debug}{geometry}&ticket={ticket}&profileId={profileId}&psEnterButton={psEnterButton}"
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientUplay": {
|
||||
"fields": {
|
||||
"ps4UplayUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&psEnterButton={psEnterButton}&platformVariant={platformVariant}",
|
||||
"ps5UplayUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&platformVariant={platformVariant}",
|
||||
"ounceUplayUrl": "https://{env}connect.cdn.ubisoft.com/overlay/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&platformVariant={platformVariant}",
|
||||
"switchUplayUrl": "https://{env}connect.cdn.ubisoft.com/overlay/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&platformVariant={platformVariant}",
|
||||
"psnUplayClientId": "171ca84a-371e-412b-a807-6531f3f1e454",
|
||||
"iosUplayWebLaunchUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ticket={ticket}&env={environment}&playerSessionId={playerSessionId}&deviceType=phone&isStandalone=false",
|
||||
"xoneUplayUWPLaunchUrl": "Ms-xbl-55C5EE27://default?isUbisoftConnect=true&baseURL={env}overlay.cdn.ubisoft.com/default/µApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}",
|
||||
"xoneUplayWebLaunchUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&platformVariant={platformVariant}",
|
||||
"macosUplayWebLaunchUrl": "https://{env}overlay.cdn.ubisoft.com/default/?spaceId={spaceId}&applicationId={applicationId}&platform=macos&locale={locale}&env={environment}&deviceType=desktop",
|
||||
"androidUplayWebLaunchUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ticket={ticket}&env={environment}&playerSessionId={playerSessionId}&deviceType=phone&isStandalone=false",
|
||||
"iosUplayWebCompletionUrl": "ubisoftconnectscheme:",
|
||||
"scarlettUplayUWPLaunchUrl": "Ms-xbl-55C5EE27://default?isUbisoftConnect=true&baseURL={env}overlay.cdn.ubisoft.com/default/µApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}",
|
||||
"scarlettUplayWebLaunchUrl": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&profileId={profileId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ubiTicket={ubiTicket}&firstPartyToken={firstPartyToken}&env={environment}&deviceType=tv&playerSessionId={playerSessionId}&platformVariant={platformVariant}",
|
||||
"xoneUplayWebCompletionUrl": "about:blank",
|
||||
"macosUplayWebCompletionUrl": "ubisoftconnectscheme:",
|
||||
"ounceUplayWebCompletionUrl": "uplayscheme:",
|
||||
"switchUplayWebCompletionUrl": "uplayscheme:",
|
||||
"androidUplayWebCompletionUrl": "ubisoftconnectscheme:",
|
||||
"iosUplayWebLaunchUrlMicroApp": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ticket={ticket}&env={environment}&playerSessionId={playerSessionId}&deviceType=phone&profileId={profileId}&isStandalone=false",
|
||||
"scarlettUplayWebCompletionUrl": "about:blank",
|
||||
"androidUplayWebLaunchUrlMicroApp": "https://{env}overlay.cdn.ubisoft.com/default/?microApp={microApp}µAppParams={microAppParams}&spaceId={spaceId}&applicationId={applicationId}&platform={platform}&locale={locale}&sdkVersion={sdkVersion}&ticket={ticket}&env={environment}&playerSessionId={playerSessionId}&deviceType=phone&profileId={profileId}&isStandalone=false"
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientUrlsPlaceholders": {
|
||||
"fields": {
|
||||
"baseurl_ws": {
|
||||
"China": "wss://public-ws-ubiservices.ubisoft.cn/",
|
||||
"Standard": "wss://{env}public-ws-ubiservices.ubi.com",
|
||||
"China_GAAP": "wss://gaap.ubiservices.ubi.com:16000"
|
||||
},
|
||||
"baseurl_aws": {
|
||||
"China": "https://public-ubiservices.ubisoft.cn",
|
||||
"Standard": "https://{env}public-ubiservices.ubi.com",
|
||||
"China_GAAP": "https://gaap.ubiservices.ubi.com:12000"
|
||||
},
|
||||
"baseurl_msr": {
|
||||
"China": "https://public-ubiservices.ubisoft.cn",
|
||||
"Standard": "https://msr-{env}public-ubiservices.ubi.com",
|
||||
"China_GAAP": "https://gaap.ubiservices.ubi.com:12000"
|
||||
}
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientLogin": {
|
||||
"fields": {
|
||||
"populationHttpRequestOptimizationEnabled": true,
|
||||
"populationHttpRequestOptimizationRetryCount": 0,
|
||||
"populationHttpRequestOptimizationRetryTimeoutIntervalMsec": 5000,
|
||||
"populationHttpRequestOptimizationRetryTimeoutIncrementMsec": 1000
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientFeaturesSwitches": {
|
||||
"fields": {
|
||||
"populationsAutomaticUpdate": true,
|
||||
"forcePrimarySecondaryStoreSyncOnReturnFromBackground": true
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientChina": {
|
||||
"fields": {
|
||||
"websocketHost": "public-ws-ubiservices.ubi.com",
|
||||
"tLogApplicationId": "",
|
||||
"tLogApplicationKey": "",
|
||||
"tLogApplicationName": ""
|
||||
},
|
||||
"relatedPopulation": null
|
||||
},
|
||||
"us-sdkClientUrls": {
|
||||
"fields": {
|
||||
"populations": "{baseurl_aws}/{version}/profiles/me/populations",
|
||||
"profilesToken": "{baseurl_aws}/{version}/profiles/{profileId}/tokens/{token}"
|
||||
},
|
||||
"relatedPopulation": null
|
||||
}
|
||||
}
|
||||
}
|
||||
1655
package-lock.json
generated
1655
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,13 @@
|
||||
"author": "PartyService",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.8.4",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^1.20.3",
|
||||
"chalk": "^5.4.1",
|
||||
"express": "^4.21.2",
|
||||
"express-session": "^1.18.1",
|
||||
"fs": "^0.0.1-security",
|
||||
"md5": "^2.3.0",
|
||||
"pm2": "^6.0.5",
|
||||
|
||||
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>
|
||||
@@ -3,8 +3,8 @@
|
||||
* Handles World Dance Floor (WDF) related routes as a plugin
|
||||
*/
|
||||
const axios = require('axios');
|
||||
const Plugin = require('../classes/Plugin'); // Assuming Plugin is located at ../core/classes/Plugin.js
|
||||
const Logger = require('../utils/logger');
|
||||
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 {
|
||||
/**
|
||||
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"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"forcePort": false,
|
||||
"isPublic": true,
|
||||
"enableSSL": true,
|
||||
"domain": "jdp.justdancenext.xyz",
|
||||
"domain": "justdanceservices.prjktla.online",
|
||||
"modName": "Just Dance Unlimited Mod",
|
||||
"serverstatus": {
|
||||
"isMaintenance": false,
|
||||
@@ -16,17 +16,17 @@
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "WDFPlugin",
|
||||
"description": "Create a fake response for WDF so that the mod can run on older version",
|
||||
"path": "{dirname}/core/wdf/FakeWdfPlugin.js",
|
||||
"execution": "init"
|
||||
},
|
||||
{
|
||||
"name": "HelloWorldPlugin",
|
||||
"description": "A simple example plugin that demonstrates the plugin system",
|
||||
"path": "{dirname}/plugins/HelloWorldPlugin.js",
|
||||
"execution": "init"
|
||||
},
|
||||
{
|
||||
"name": "AdminPanelPlugin",
|
||||
"description": "Secure admin panel for server management and maintenance",
|
||||
"path": "{dirname}/plugins/AdminPanelPlugin.js",
|
||||
"execution": "init"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user