reldens
Version:
Reldens - MMORPG Platform
1,142 lines (1,096 loc) • 68 kB
JavaScript
/**
*
* Reldens - AdminManager
*
*/
const { UploaderFactory } = require('./uploader-factory');
const { AdminTranslations } = require('./admin-translations');
const { AdminEntitiesGenerator } = require('./admin-entities-generator');
const { AdminManagerConfig } = require('./admin-manager-config');
const { AdminDistHelper } = require('./admin-dist-helper');
const { FileHandler } = require('../../game/server/file-handler');
const { AllowedFileTypes } = require('../../game/allowed-file-types');
const { PageRangeProvider } = require('../../game/page-range-provider');
const { GameConst } = require('../../game/constants');
const { Logger, sc } = require('@reldens/utils');
const {
RandomMapGenerator,
LayerElementsObjectLoader,
LayerElementsCompositeLoader,
MultipleByLoaderGenerator,
MultipleWithAssociationsByLoaderGenerator
} = require('@reldens/tile-map-generator');
class AdminManager
{
dataServerConfig = null;
dataServer = null;
events = null;
loginManager = null;
app = null;
applicationFramework = null;
fileStorageManager = null;
mapsImporter = null;
objectsImporter = null;
skillsImporter = null;
bodyParser = null;
session = null;
broadcastCallback = null;
gameServer = null;
installer = null;
config = null;
themeManager = null;
secret = '';
useSecureLogin = false;
rootPath = '';
adminRoleId = 0;
buildAdminScriptsOnActivation = null;
buildAdminCssOnActivation = null;
buckets = null;
shutdownTimeout = null;
shuttingDownIn = 0;
adminContents = {};
blackList = {};
constructor(adminManagerConfig)
{
// @TODO - BETA - Refactor, split class in multiple services.
if(!(adminManagerConfig instanceof AdminManagerConfig)){
Logger.error('The adminManagerConfig param must be an instance of AdminManagerConfig.');
return false;
}
if(!adminManagerConfig.validate()){
return false;
}
adminManagerConfig.assignProperties(this);
this.logoutPath = '/logout';
this.loginPath = '/login';
this.viewPath = '/view';
this.editPath = '/edit';
this.savePath = '/save';
this.deletePath = '/delete';
this.managementPath = '/management';
this.mapsWizardPath = '/maps-wizard';
this.objectsImportPath = '/objects-import';
this.skillsImportPath = '/skills-import';
this.adminEntitiesGenerator = new AdminEntitiesGenerator();
this.uploaderFactory = new UploaderFactory();
this.mapsWizardHandlers = {
'elements-object-loader': LayerElementsObjectLoader,
'elements-composite-loader': LayerElementsCompositeLoader,
'multiple-by-loader': MultipleByLoaderGenerator,
'multiple-with-association-by-loader': MultipleWithAssociationsByLoaderGenerator
};
}
async setupAdmin()
{
if(!this.installer.isInstalled()){
Logger.info('Reldens is not installed, administration panel will not be available.');
return;
}
this.secret = (process.env.RELDENS_ADMIN_SECRET || '').toString();
this.useSecureLogin = Boolean(Number(process.env.RELDENS_ADMIN_SECURE_LOGIN || 0) || false);
this.rootPath = process.env.RELDENS_ADMIN_ROUTE_PATH || '/reldens-admin';
this.entities = this.adminEntitiesGenerator.generate(
this.dataServerConfig.loadedEntities,
this.dataServer.entityManager.entities
);
this.resourcesByReference = {};
this.resources = this.prepareResources(this.entities);
this.relations = this.prepareRelations(this.entities);
this.buckets = this.fetchThemesFolders();
this.adminRoleId = this.config.get('server/admin/roleId', 1);
this.buildAdminCssOnActivation = this.config.getWithoutLogs('server/admin/buildAdminCssOnActivation', true);
this.buildAdminScriptsOnActivation = this.config.getWithoutLogs(
'server/admin/buildAdminScriptsOnActivation',
true
);
this.translations = AdminTranslations.appendTranslations(this.dataServerConfig?.translations || {});
this.stylesFilePath = this.config.getWithoutLogs(
'server/admin/stylesPath',
'/css/'+GameConst.STRUCTURE.ADMIN_CSS_FILE
);
this.scriptsFilePath = this.config.getWithoutLogs(
'server/admin/scriptsPath',
'/'+GameConst.STRUCTURE.ADMIN_JS_FILE
);
this.autoSyncDist = this.config.getWithoutLogs('server/admin/autoSyncDist', true);
this.branding = {
companyName: this.config.getWithoutLogs('server/admin/companyName', 'Reldens - Administration Panel'),
logo: this.config.getWithoutLogs('server/admin/logoPath', '/assets/web/reldens-your-logo-mage.png'),
favicon: this.config.getWithoutLogs('server/admin/faviconPath', '/assets/web/favicon.ico'),
copyRight: this.config.getWithoutLogs(
'server/admin/copyRight',
await FileHandler.fetchFileContents(
FileHandler.joinPaths(
this.themeManager.projectAdminTemplatesPath,
this.themeManager.adminTemplatesList.defaultCopyRight
)
)
)
};
this.adminFilesContents = await this.fetchAdminFilesContents(this.themeManager.adminTemplates);
if(!this.adminFilesContents){
return;
}
await this.buildAdminContents();
await this.buildAdminScripts();
await this.buildAdminCss();
this.setupAdminRoutes();
this.setupEntitiesRoutes();
}
fetchThemesFolders()
{
let allFolders = FileHandler.fetchSubFoldersList(this.themeManager.themePath);
let pluginsIndex = allFolders.indexOf('plugins');
if(-1 !== pluginsIndex){
allFolders.splice(pluginsIndex, 1);
}
return allFolders;
}
async buildAdminContents()
{
this.adminContents.layout = await this.buildLayout();
this.adminContents.sideBar = await this.buildSideBar();
this.adminContents.login = await this.buildLogin();
this.adminContents.dashboard = await this.buildDashboard();
this.adminContents.management = await this.buildManagement();
this.adminContents.mapsWizard = await this.buildMapsWizard();
this.adminContents.objectsImport = await this.buildObjectsImport();
this.adminContents.skillsImport = await this.buildSkillsImport();
this.adminContents.entities = await this.buildEntitiesContents();
}
async buildLayout()
{
return await this.render(
this.adminFilesContents.layout,
{
sideBar: '{{&sideBar}}',
pageContent: '{{&pageContent}}',
stylesFilePath: this.stylesFilePath,
scriptsFilePath: this.scriptsFilePath,
rootPath: this.rootPath,
brandingCompanyName: this.branding.companyName,
copyRight: this.branding.copyRight
}
);
}
async buildSideBar()
{
let navigationContents = {
'Wizards': {
[this.translations.labels['mapsWizard']]: await this.render(
this.adminFilesContents.sideBarItem,
{name: this.translations.labels['mapsWizard'], path: this.rootPath+this.mapsWizardPath}
),
[this.translations.labels['objectsImport']]: await this.render(
this.adminFilesContents.sideBarItem,
{name: this.translations.labels['objectsImport'], path: this.rootPath+this.objectsImportPath}
),
[this.translations.labels['skillsImport']]: await this.render(
this.adminFilesContents.sideBarItem,
{name: this.translations.labels['skillsImport'], path: this.rootPath+this.skillsImportPath}
)
}
};
for(let driverResource of this.resources){
let navigation = driverResource.options?.navigation;
let name = this.translations.labels[driverResource.id()] || this.translations.labels[driverResource.entityKey];
let path = this.rootPath+'/'+(driverResource.id().replace(/_/g, '-'));
if(navigation?.name){
if(!navigationContents[navigation.name]){
navigationContents[navigation.name] = {};
}
navigationContents[navigation.name][driverResource.id()] = await this.render(
this.adminFilesContents.sideBarItem,
{name, path}
);
continue;
}
navigationContents[driverResource.id()] = await this.render(
this.adminFilesContents.sideBarItem,
{name, path}
);
}
navigationContents['Server'] = {'Management': await this.render(
this.adminFilesContents.sideBarItem,
{name: this.translations.labels['management'], path: this.rootPath+this.managementPath}
)};
await this.events.emitSync('adminSideBar', {navigationContents});
let navigationView = '';
for(let id of Object.keys(navigationContents)){
if(sc.isObject(navigationContents[id])){
let subItems = '';
for(let subId of Object.keys(navigationContents[id])){
subItems += navigationContents[id][subId];
}
navigationView += await this.render(
this.adminFilesContents.sideBarHeader,
{name: id, subItems}
);
continue;
}
navigationView += navigationContents[id];
}
return await this.render(
this.adminFilesContents.sideBar,
{
rootPath: this.rootPath,
navigationView
}
);
}
async buildLogin()
{
return await this.renderRoute(this.adminFilesContents.login, '');
}
async buildDashboard()
{
return await this.renderRoute(this.adminFilesContents.dashboard, this.adminContents.sideBar);
}
async buildManagement()
{
let pageContent = await this.render(
this.adminFilesContents.management,
{
actionPath: this.rootPath+this.managementPath,
shutdownTime: this.config.getWithoutLogs('server/shutdownTime', 180),
shuttingDownLabel: '{{&shuttingDownLabel}}',
shuttingDownTime: '{{&shuttingDownTime}}',
submitLabel: '{{&submitLabel}}',
submitType: '{{&submitType}}'
}
);
return await this.renderRoute(pageContent, this.adminContents.sideBar);
}
async buildMapsWizard()
{
let pageContent = await this.render(
this.adminFilesContents.mapsWizard,
{
actionPath: this.rootPath+this.mapsWizardPath
}
);
return await this.renderRoute(pageContent, this.adminContents.sideBar);
}
async buildObjectsImport()
{
let pageContent = await this.render(
this.adminFilesContents.objectsImport,
{
actionPath: this.rootPath+this.objectsImportPath
}
);
return await this.renderRoute(pageContent, this.adminContents.sideBar);
}
async buildSkillsImport()
{
let pageContent = await this.render(
this.adminFilesContents.skillsImport,
{
actionPath: this.rootPath+this.skillsImportPath
}
);
return await this.renderRoute(pageContent, this.adminContents.sideBar);
}
async buildEntitiesContents()
{
let entitiesContents = {};
for(let driverResource of this.resources){
let templateTitle = this.translations.labels[driverResource.id()];
let entityName = (driverResource.id().replace(/_/g, '-'));
let entityListRoute = this.rootPath+'/'+entityName;
let entityEditRoute = entityListRoute+this.editPath;
let entitySaveRoute = entityListRoute+this.savePath;
let entityDeleteRoute = entityListRoute+this.deletePath;
let uploadProperties = this.fetchUploadProperties(driverResource);
let multipartFormData = 0 < Object.keys(uploadProperties).length ? ' enctype="multipart/form-data"' : '';
let idProperty = this.fetchEntityIdPropertyKey(driverResource);
let editProperties = Object.keys(driverResource.options.properties);
editProperties.splice(editProperties.indexOf(idProperty), 1);
entitiesContents[entityName] = {
list: await this.render(
this.adminFilesContents.list,
{
entityName,
templateTitle,
entityListRoute,
entityEditRoute,
filters: driverResource.options.filterProperties.map((property) => {
let propertyName = property.replace(/_/g, ' ');
let name = propertyName.slice(0, 1).toUpperCase() + propertyName.slice(1).toLowerCase();
return {
propertyKey: property,
name,
value: '{{&'+property+'}}'
};
}),
list: '{{&list}}',
pagination: '{{&pagination}}'
}
),
view: await this.render(
this.adminFilesContents.view,
{
entityName,
templateTitle,
fields: driverResource.options.showProperties.map((property) => {
let propertyName = property.replace(/_/g, ' ');
let name = propertyName.slice(0, 1).toUpperCase() + propertyName.slice(1).toLowerCase();
return {
name,
value: '{{&'+property+'}}'
};
}),
entityDeleteRoute,
id: '{{&id}}',
entityListRoute,
entityEditRoute: '{{&entityEditRoute}}'
}
),
edit: await this.render(
this.adminFilesContents.edit,
{
entityName,
entitySaveRoute,
idValue: '{{&idValue}}',
idProperty: '{{&idProperty}}',
templateTitle: '{{&templateTitle}}',
entityViewRoute: '{{&entityViewRoute}}',
multipartFormData,
editFields: editProperties.map((property) => {
let propertyName = property.replace(/_/g, ' ');
let name = propertyName.slice(0, 1).toUpperCase() + propertyName.slice(1).toLowerCase();
return {
name,
value: '{{&'+property+'}}'
};
})
}
)
};
}
return entitiesContents;
}
fetchUploadProperties(driverResource)
{
if(!driverResource.options.uploadProperties){
driverResource.options.uploadProperties = {};
for(let propertyKey of Object.keys(driverResource.options.properties)){
let property = driverResource.options.properties[propertyKey];
if(property.isUpload){
driverResource.options.uploadProperties[propertyKey] = property;
}
}
}
return driverResource.options.uploadProperties;
}
async render(content, params)
{
return await this.themeManager.templateEngine.render(content, params);
}
async renderRoute(pageContent, sideBar)
{
return await this.render(
this.adminContents.layout,
{
stylesFilePath: this.stylesFilePath,
scriptsFilePath: this.scriptsFilePath,
brandingCompanyName: this.branding.companyName,
copyRight: this.branding.copyRight,
pageContent,
sideBar
}
);
}
async fetchAdminFilesContents(adminTemplates)
{
let adminFilesContents = {};
for(let template of Object.keys(adminTemplates)){
let templateData = adminTemplates[template];
if(sc.isObject(templateData)){
let subFoldersContents = await this.fetchAdminFilesContents(templateData);
if(!subFoldersContents){
return false;
}
adminFilesContents[template] = subFoldersContents;
continue;
}
if(!FileHandler.isFile(templateData)){
Logger.critical('Admin template file not found.', template);
return false;
}
adminFilesContents[template] = await FileHandler.fetchFileContents(templateData);
}
return adminFilesContents;
}
async buildAdminScripts()
{
if(!this.buildAdminScriptsOnActivation){
return;
}
await this.themeManager.buildAdminScripts();
}
async buildAdminCss()
{
if(!this.buildAdminCssOnActivation){
return;
}
await this.themeManager.buildAdminCss();
}
prepareResources(rawResources)
{
let rawResourcesKeys = Object.keys(rawResources);
if(!rawResources || 0 === rawResourcesKeys.length){
return [];
}
let registeredResources = [];
for(let i of rawResourcesKeys){
let rawResource = rawResources[i];
let tableName = rawResource.rawEntity.tableName();
// @TODO - BETA - Refactor to add the ID property and composed labels (id + label), in the resource.
let driverResource = {
id: () => {
return tableName;
},
entityKey: i,
entityPath: (tableName.replace(/_/g, '-')),
options: {
navigation: sc.hasOwn(rawResource.config, 'parentItemLabel') ? {
name: rawResource.config.parentItemLabel,
icon: rawResource.config.icon || 'List'
} : null,
listProperties: rawResource.config.listProperties || [],
showProperties: rawResource.config.showProperties || [],
filterProperties: rawResource.config.filterProperties || [],
editProperties: rawResource.config.editProperties || [],
properties: rawResource.config.properties || {},
titleProperty: sc.get(rawResource.config, 'titleProperty', null),
sort: sc.get(rawResource.config, 'sort', null)
},
};
this.resourcesByReference[tableName] = driverResource;
registeredResources.push(driverResource);
}
return registeredResources;
}
prepareRelations()
{
// @TODO - BETA - Refactor, include in resources generation at once.
let registeredRelations = {};
for(let resource of this.resources){
for(let propertyKey of Object.keys(resource.options.properties)){
let property = resource.options.properties[propertyKey];
if('reference' !== property.type){
continue;
}
let relationResource = this.resources.filter((resource) => {
return resource.id() === property.reference;
}).shift();
let relationKey = property.alias || property.reference;
let titleProperty = relationResource?.options?.titleProperty;
if(!titleProperty){
continue;
}
if(!registeredRelations[property.reference]){
registeredRelations[property.reference] = {};
}
registeredRelations[property.reference][relationKey] = titleProperty;
}
}
return registeredRelations;
}
setupAdminRoutes()
{
// apply session middleware only to /admin routes:
this.adminRouter = this.applicationFramework.Router();
this.adminRouter.use(this.session({secret: this.secret, resave: false, saveUninitialized: true}));
this.adminRouter.use(this.bodyParser.json());
// route for the login page:
this.adminRouter.get(this.loginPath, async (req, res) => {
return res.send(this.adminContents.login);
});
// route for handling login:
this.adminRouter.post(this.loginPath, async (req, res) => {
let { email, password } = req.body;
let loginResult = await this.loginManager.roleAuthenticationCallback(email, password, this.adminRoleId);
if(loginResult){
req.session.user = loginResult;
return res.redirect(this.rootPath);
}
return res.redirect(this.rootPath+this.loginPath+'?login-error=true');
});
// route for the admin panel dashboard:
this.adminRouter.get('/', this.isAuthenticated.bind(this), async (req, res) => {
return res.send(this.adminContents.dashboard);
});
// route for logout:
this.adminRouter.get(this.logoutPath, (req, res) => {
req.session.destroy();
res.redirect(this.rootPath+this.loginPath);
});
// management routes:
this.adminRouter.get(this.managementPath, this.isAuthenticated.bind(this), async (req, res) => {
let management = this.adminContents.management;
let rendererContent = await this.render(management, this.fetchShuttingDownData());
return res.send(rendererContent);
});
this.adminRouter.post(this.managementPath, this.isAuthenticated.bind(this), async (req, res) => {
this.shutdownTime = req.body['shutdown-time'];
let redirectManagementPath = this.rootPath+this.managementPath;
if(!this.shutdownTime){
return res.redirect(redirectManagementPath+'?result=shutdownError');
}
if(0 < this.shuttingDownIn){
clearInterval(this.shutdownInterval);
clearTimeout(this.shutdownTimeout);
this.shuttingDownIn = 0;
return res.redirect(redirectManagementPath+'?result=success');
}
await this.broadcastShutdownMessage();
this.shutdownTimeout = setTimeout(
async () => {
Logger.info('Server is shutting down by request on the administration panel.', sc.getTime());
if(this.broadcastCallback && sc.isFunction(this.broadcastCallback)){
await this.broadcastCallback({message: 'Server Offline.'});
}
throw new Error('Server shutdown by request on the administration panel.');
},
this.shutdownTime * 1000
);
this.shutdownInterval = setInterval(
async () => {
this.shuttingDownIn--;
Logger.info('Server is shutting down in '+this.shuttingDownIn+' seconds.');
if(
0 < this.shuttingDownIn
&& (this.shuttingDownIn <= 5 || Math.ceil(this.shutdownTime / 2) === this.shuttingDownIn)
){
await this.broadcastShutdownMessage();
}
if(0 === this.shuttingDownIn){
Logger.info('Server OFF at: '+ sc.getTime());
clearInterval(this.shutdownInterval);
}
},
1000
);
this.shuttingDownIn = this.shutdownTime;
return res.redirect(redirectManagementPath+'?result=success');
});
this.setupMapsWizardRoutes();
this.setupObjectsImporterRoutes();
this.setupSkillsImporterRoutes();
// apply the adminRouter to the /admin path:
this.app.use(this.rootPath, this.adminRouter);
}
setupMapsWizardRoutes()
{
// set generated paths to be available in the admin
this.adminRouter.use(
'/generate-data',
this.isAuthenticated.bind(this),
this.applicationFramework.static(this.themeManager.projectGenerateDataPath)
);
this.adminRouter.use(
'/generated',
this.isAuthenticated.bind(this),
this.applicationFramework.static(this.themeManager.projectGeneratedDataPath)
);
Logger.info(
'Included administration panel static routes.',
this.themeManager.projectGenerateDataPath,
this.themeManager.projectGeneratedDataPath
);
// step-1, initial wizard options:
this.adminRouter.get(this.mapsWizardPath, this.isAuthenticated.bind(this), async (req, res) => {
let rendererContent = await this.render(this.adminContents.mapsWizard, this.fetchShuttingDownData());
return res.send(rendererContent);
});
let fields = [
{name: 'generatorImages'},
{name: 'generatorJsonFiles'}
];
let buckets = {
generatorImages: this.themeManager.projectGenerateDataPath,
generatorJsonFiles: this.themeManager.projectGenerateDataPath
};
let allowedFileTypes = {
generatorImages: AllowedFileTypes.IMAGE,
generatorJsonFiles: AllowedFileTypes.TEXT
};
this.adminRouter.post(
this.mapsWizardPath,
this.isAuthenticated.bind(this),
this.uploaderFactory.createUploader(fields, buckets, allowedFileTypes),
async (req, res) => {
// step-2, upload and maps generation:
if('generate' === req?.body?.mainAction){
return await this.generateMaps(req, res);
}
// step-3, maps selection and import:
if('import' === req?.body?.mainAction){
return res.redirect(await this.importSelectedMaps(req));
}
}
);
}
setupObjectsImporterRoutes()
{
// step-1, import options:
this.adminRouter.get(this.objectsImportPath, this.isAuthenticated.bind(this), async (req, res) => {
let rendererContent = await this.render(this.adminContents.objectsImport, this.fetchShuttingDownData());
return res.send(rendererContent);
});
let fields = [{name: 'generatorJsonFiles'}];
let buckets = {generatorJsonFiles: this.themeManager.projectGeneratedDataPath};
let allowedFileTypes = {generatorJsonFiles: AllowedFileTypes.TEXT};
this.adminRouter.post(
this.objectsImportPath,
this.isAuthenticated.bind(this),
this.uploaderFactory.createUploader(fields, buckets, allowedFileTypes),
async (req, res) => {
// step-2, import:
return res.redirect(await this.importObjects(req));
}
);
}
setupSkillsImporterRoutes()
{
// step-1, import options:
this.adminRouter.get(this.skillsImportPath, this.isAuthenticated.bind(this), async (req, res) => {
let rendererContent = await this.render(this.adminContents.skillsImport, this.fetchShuttingDownData());
return res.send(rendererContent);
});
let fields = [{name: 'generatorJsonFiles'}];
let buckets = {generatorJsonFiles: this.themeManager.projectGeneratedDataPath};
let allowedFileTypes = {generatorJsonFiles: AllowedFileTypes.TEXT};
this.adminRouter.post(
this.skillsImportPath,
this.isAuthenticated.bind(this),
this.uploaderFactory.createUploader(fields, buckets, allowedFileTypes),
async (req, res) => {
// step-2, import:
return res.redirect(await this.importSkills(req));
}
);
}
async generateMaps(req, res)
{
let selectedHandler = req?.body?.mapsWizardAction;
if(!selectedHandler){
return this.mapsWizardRedirect(res, 'mapsWizardMissingActionError');
}
let generatorData = req?.body?.generatorData;
if(!generatorData){
return this.mapsWizardRedirect(res, 'mapsWizardMissingDataError');
}
let mapData = sc.toJson(generatorData);
if(!mapData){
return this.mapsWizardRedirect(res, 'mapsWizardWrongJsonDataError');
}
let handler = this.mapsWizardHandlers[selectedHandler];
if(!handler){
return this.mapsWizardRedirect(res, 'mapsWizardMissingHandlerError');
}
let generatorWithData = false;
let generatedMap = false;
try {
let handlerParams = {mapData, rootFolder: this.themeManager.projectGenerateDataPath};
if('elements-object-loader' === selectedHandler){
let loader = new handler(handlerParams);
await loader.load();
let generator = new RandomMapGenerator(loader.mapData);
generatedMap = await generator.generate();
generatorWithData = generator;
}
if('elements-composite-loader' === selectedHandler){
let loader = new handler(handlerParams);
await loader.load();
let generator = new RandomMapGenerator();
await generator.fromElementsProvider(loader.mapData);
generatedMap = await generator.generate();
generatorWithData = generator;
}
if('multiple-by-loader' === selectedHandler){
let generator = new MultipleByLoaderGenerator({loaderData: handlerParams});
await generator.generate();
generatorWithData = generator;
}
if('multiple-with-association-by-loader' === selectedHandler){
let generator = new MultipleWithAssociationsByLoaderGenerator({loaderData: handlerParams});
await generator.generate();
generatorWithData = generator;
}
} catch (error) {
Logger.error('Maps generator error.', selectedHandler, generatorData, error);
return this.mapsWizardRedirect(res, 'mapsWizardGeneratorError');
}
if(!generatorWithData){
Logger.error('Maps not generated, incompatible selected handler.', selectedHandler, generatorData);
return this.mapsWizardRedirect(res, 'mapsWizardSelectedHandlerError');
}
let mapsData = {
maps: [],
actionPath: this.rootPath+this.mapsWizardPath,
generatedMapsHandler: selectedHandler,
importAssociationsForChangePoints: Number(mapData.importAssociationsForChangePoints || 0),
importAssociationsRecursively: Number(mapData.importAssociationsRecursively || 0),
verifyTilesetImage: Number(mapData.verifyTilesetImage || 1),
automaticallyExtrudeMaps: Number(mapData.automaticallyExtrudeMaps || 1)
};
if(generatedMap){
let tileWidth = generatedMap.tilewidth;
let tileHeight = generatedMap.tileheight;
let mapFileName = generatorWithData.mapFileName;
if(-1 === mapFileName.indexOf('json')){
mapFileName = mapFileName+'.json';
}
mapsData.maps.push({
key: generatorWithData.mapName,
mapWidth: generatedMap.width * tileWidth,
mapHeight: generatedMap.height * tileHeight,
tileWidth,
tileHeight,
mapImage: this.rootPath+'/generated/'+generatorWithData.tileSheetName,
mapJson: this.rootPath+'/generated/'+mapFileName
});
}
if(generatorWithData.generators && generatorWithData.generatedMaps){
for(let i of Object.keys(generatorWithData.generators)){
let generator = generatorWithData.generators[i];
let generatedMap = generatorWithData.generatedMaps[generator.mapName];
let tileWidth = generatedMap.tilewidth;
let tileHeight = generatedMap.tileheight;
let mapFileName = generator.mapFileName;
if(-1 === mapFileName.indexOf('json')){
mapFileName = mapFileName+'.json';
}
mapsData.maps.push({
key: generator.mapName,
mapWidth: generatedMap.width * tileWidth,
mapHeight: generatedMap.height * tileHeight,
tileWidth,
tileHeight,
mapImage: this.rootPath+'/generated/'+generator.tileSheetName,
mapJson: this.rootPath+'/generated/'+mapFileName
});
}
}
if(0 === mapsData.maps.length){
return this.mapsWizardRedirect(res, 'mapsWizardMapsNotGeneratedError');
}
return this.mapsWizardMapsSelection(res, mapsData);
}
mapsWizardRedirect(res, result)
{
return res.redirect(this.rootPath + this.mapsWizardPath + '?result='+result);
}
async mapsWizardMapsSelection(res, data)
{
let renderedView = await this.render(this.adminFilesContents.mapsWizardMapsSelection, data);
return res.send(await this.renderRoute(renderedView, this.adminContents.sideBar));
}
async importSelectedMaps(req)
{
let generatedMapData = this.mapGeneratedMapsDataForImport(req.body);
if(!generatedMapData){
return this.rootPath+this.mapsWizardPath+'?result=mapsWizardImportDataError';
}
let importResult = await this.mapsImporter.import(generatedMapData);
if(!importResult){
let errorCode = this.mapsImporter.errorCode || 'mapsWizardImportError'
return this.rootPath+this.mapsWizardPath+'?result='+errorCode;
}
return this.rootPath+this.mapsWizardPath+'?result=success';
}
async importObjects(req)
{
let generateObjectsData = sc.toJson(req?.body?.generatorData);
if(!generateObjectsData){
let fileName = req.files?.generatorJsonFiles?.shift()?.originalname;
if(!fileName){
return this.rootPath+this.skillsImportPath+'?result=objectsImportMissingDataError';
}
generateObjectsData = sc.toJson(await FileHandler.fetchFileContents(
FileHandler.joinPaths(this.themeManager.projectGeneratedDataPath, fileName)
));
if(!generateObjectsData){
return this.rootPath+this.objectsImportPath+'?result=objectsImportDataError';
}
}
let importResult = await this.objectsImporter.import(generateObjectsData);
if(!importResult){
let errorCode = this.objectsImporter.errorCode || 'objectsImportError'
return this.rootPath+this.objectsImportPath+'?result='+errorCode;
}
return this.rootPath+this.objectsImportPath+'?result=success';
}
async importSkills(req)
{
let generateSkillsData = sc.toJson(req?.body?.generatorData);
if(!generateSkillsData){
let fileName = req.files?.generatorJsonFiles?.shift()?.originalname;
if(!fileName){
return this.rootPath+this.skillsImportPath+'?result=skillsImportMissingDataError';
}
generateSkillsData = sc.toJson(await FileHandler.fetchFileContents(
FileHandler.joinPaths(this.themeManager.projectGeneratedDataPath, fileName)
));
if(!generateSkillsData){
return this.rootPath+this.skillsImportPath+'?result=skillsImportDataError';
}
}
let importResult = await this.skillsImporter.import(generateSkillsData);
if(!importResult){
let errorCode = this.skillsImporter.errorCode || 'skillsImportError'
return this.rootPath+this.skillsImportPath+'?result='+errorCode;
}
return this.rootPath+this.skillsImportPath+'?result=success';
}
mapGeneratedMapsDataForImport(data)
{
if(!data.selectedMaps){
return false;
}
let importAssociations = 'multiple-with-association-by-loader' === data.generatedMapsHandler;
let mappedData = {
importAssociationsForChangePoints: importAssociations,
importAssociationsRecursively: importAssociations,
automaticallyExtrudeMaps: data.automaticallyExtrudeMaps,
verifyTilesetImage: data.verifyTilesetImage,
relativeGeneratedDataPath: 'generate-data/generated',
maps: {}
};
for(let mapKey of data.selectedMaps){
mappedData.maps[data['map-title-'+mapKey]] = mapKey; // for example: {'Town 1': 'town-001'}
}
return mappedData;
}
async broadcastShutdownMessage()
{
let shuttingDownTime = 0 === this.shuttingDownIn ? this.shutdownTime : this.shuttingDownIn;
await this.broadcastSystemMessage('Server is shutting down in ' + shuttingDownTime + ' seconds.');
}
async broadcastSystemMessage(message)
{
if(!this.broadcastCallback || !sc.isFunction(this.broadcastCallback)){
return;
}
await this.broadcastCallback({message});
}
fetchShuttingDownData()
{
if(0 === this.shuttingDownIn){
return {
shuttingDownLabel: '',
shuttingDownTime: '',
submitLabel: this.translations.labels.submitShutdownLabel || 'Shutdown Server',
submitType: 'danger',
};
}
return {
shuttingDownLabel: this.translations.labels.shuttingDown || '',
shuttingDownTime: this.shuttingDownIn || '',
submitLabel: this.translations.labels.submitCancelLabel || 'Cancel Server Shutdown',
submitType: 'warning',
};
}
setupEntitiesRoutes()
{
if(!this.resources || 0 === this.resources.length){
return;
}
for(let driverResource of this.resources){
let entityPath = driverResource.entityPath;
let entityRoute = '/'+entityPath;
this.adminRouter.get(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
return res.send(routeContents);
});
this.adminRouter.post(entityRoute, this.isAuthenticated.bind(this), async (req, res) => {
let routeContents = await this.generateListRouteContent(req, driverResource, entityPath);
return res.send(routeContents);
});
this.adminRouter.get(entityRoute+this.viewPath, this.isAuthenticated.bind(this), async (req, res) => {
let routeContents = await this.generateViewRouteContent(req, driverResource, entityPath);
if('' === routeContents){
return res.redirect(this.rootPath+'/'+entityPath+'?result=errorView');
}
return res.send(routeContents);
});
this.adminRouter.get(entityRoute+this.editPath, this.isAuthenticated.bind(this), async (req, res) => {
let routeContents = await this.generateEditRouteContent(req, driverResource, entityPath);
if('' === routeContents){
return res.redirect(this.rootPath+'/'+entityPath+'?result=errorEdit');
}
return res.send(routeContents);
});
this.setupSavePath(entityRoute, driverResource, entityPath);
this.adminRouter.post(entityRoute+this.deletePath, this.isAuthenticated.bind(this), async (req, res) => {
let redirectResult = await this.processDeleteEntities(req, res, driverResource, entityPath);
return res.redirect(redirectResult);
});
}
}
setupSavePath(entityRoute, driverResource, entityPath)
{
let uploadProperties = this.fetchUploadProperties(driverResource);
let uploadPropertiesKeys = Object.keys(uploadProperties || {});
if(0 === uploadPropertiesKeys.length){
this.adminRouter.post(
entityRoute+this.savePath,
this.isAuthenticated.bind(this),
async (req, res) => {
let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
return res.redirect(redirectResult);
}
);
return;
}
let fields = [];
let buckets = {};
let allowedFileTypes = {};
for(let uploadPropertyKey of uploadPropertiesKeys){
let property = uploadProperties[uploadPropertyKey];
allowedFileTypes[uploadPropertyKey] = property.allowedTypes || false;
let field = {name: uploadPropertyKey};
if(!property.isArray){
field.maxCount = 1;
}
fields.push(field);
buckets[uploadPropertyKey] = property.bucket;
}
this.adminRouter.post(
entityRoute + this.savePath,
this.isAuthenticated.bind(this),
this.uploaderFactory.createUploader(fields, buckets, allowedFileTypes),
async (req, res) => {
let redirectResult = await this.processSaveEntity(req, res, driverResource, entityPath);
return res.redirect(redirectResult);
}
);
}
async processDeleteEntities(req, res, driverResource, entityPath)
{
let ids = req?.body?.ids;
if('string' === typeof ids){
ids = ids.split(',');
}
let redirectPath = this.rootPath+'/'+entityPath+'?result=';
let resultString = 'errorMissingId';
if(!ids || 0 === ids.length){
return redirectPath + resultString;
}
try {
let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
let idProperty = this.fetchEntityIdPropertyKey(driverResource);
let idsFilter = {[idProperty]: {operator: 'IN', value: ids}};
let loadedEntities = await entityRepository.load(idsFilter);
await this.deleteEntitiesRelatedFiles(driverResource, loadedEntities);
let deleteResult = await entityRepository.delete(idsFilter);
resultString = deleteResult ? 'success' : 'errorStorageFailure';
} catch (error) {
resultString = 'errorDeleteFailure';
}
return redirectPath + resultString;
}
async deleteEntitiesRelatedFiles(driverResource, entities)
{
let resourcePropertiesKeys = Object.keys(driverResource.options.properties);
for(let propertyKey of resourcePropertiesKeys){
let property = driverResource.options.properties[propertyKey];
if(!property.isUpload){
continue;
}
for(let entity of entities){
if(!property.isArray){
await FileHandler.deleteFile(
FileHandler.joinPaths((property.bucket || ''), entity[propertyKey])
);
continue;
}
let entityFiles = entity[propertyKey].split(property.isArray);
for(let entityFile of entityFiles){
await FileHandler.deleteFile(
FileHandler.joinPaths((property.bucket || ''), entityFile)
);
}
}
}
}
async processSaveEntity(req, res, driverResource, entityPath)
{
let idProperty = this.fetchEntityIdPropertyKey(driverResource);
let id = (req?.body[idProperty] || '').toString();
let entityRepository = this.dataServer.getEntity(driverResource.entityKey);
let resourceProperties = driverResource.options.properties;
let entityDataPatch = this.preparePatchData(driverResource, idProperty, req, resourceProperties, id);
if(!entityDataPatch){
Logger.error('Bad patch data.', entityDataPatch);
return this.rootPath+'/'+entityPath+'?error=saveBadPatchData';
}
let editRoute = this.generateEntityRoute('editPath', driverResource, idProperty);
try {
let saveResult = await this.saveEntity(id, entityRepository, entityDataPatch);
if(!saveResult){
return editRoute+'&result=saveEntityStorageError';
}
if(this.autoSyncDist){
let uploadProperties = this.fetchUploadProperties(driverResource);
if(0 < Object.keys(uploadProperties).length){
for(let uploadPropertyKey of Object.keys(uploadProperties)){
let property = uploadProperties[uploadPropertyKey];
await AdminDistHelper.copyBucketFilesToDist(
property.bucket,
saveResult[uploadPropertyKey],
property.distFolder
);
}
}
}
return this.generateEntityRoute('viewPath', driverResource, idProperty, saveResult) +'&result=success';
} catch (error) {
return editRoute+'&result=saveEntityError';
}
}
async saveEntity(id, entityRepository, entityDataPatch)
{
if('' === id){
return entityRepository.create(entityDataPatch);
}
return entityRepository.updateById(id, entityDataPatch);
}
preparePatchData(driverResource, idProperty, req, resourceProperties, id)
{
let entityDataPatch = {};
for(let i of driverResource.options.editProperties){
if(i === idProperty){
continue;
}
let propertyUpdateValue = sc.get(req.body, i, null);
let property = resourceProperties[i];
let isNullValue = null === propertyUpdateValue;
let propertyType = property.type || 'string';
if(property.isUpload){
propertyType = 'upload';
propertyUpdateValue = this.prepareUploadPatchData(req, i, propertyUpdateValue, property);
}
if('number' === propertyType && !isNullValue){
propertyUpdateValue = Number(propertyUpdateValue);
}
if('string' === propertyType && !isNullValue){
propertyUpdateValue = String(propertyUpdateValue);
}
if('boolean' === propertyType){
propertyUpdateValue = Boolean(propertyUpdateValue);
}
let isUploadCreate = property.isUpload && !id;
if(property.isRequired && null === propertyUpdateValue && (!property.isUpload || isUploadCreate)){
// missing required fields would break the update:
Logger.critical('Bad patch data on update.', propertyUpdateValue, property);
return false;
}
if(!property.isUpload || (property.isUpload && null !== propertyUpdateValue)){
entityDataPatch[i] = propertyUpdateValue;
}
}
return entityDataPatch;